Imagine you’re navigating a vast ocean of changes, with commits appearing and disappearing as you refine your work. How do you pinpoint that one crucial commit? How do you select a specific range of changes for a rebase or a diff? In Jujutsu (jj), this precision navigation is handled by a powerful query language called Revsets.
This chapter will introduce you to the world of Revsets, jj’s flexible and intuitive way to refer to specific commits or groups of commits. We’ll explore the fundamental building blocks of Revsets, from simple references to complex queries, and show you how to use them to interact with your repository’s history with surgical accuracy. Mastering Revsets is crucial for leveraging jj’s mutable history, enabling you to manipulate your changes effectively and adopt advanced branchless workflows.
Before we dive in, ensure you’re comfortable with basic jj commands like jj init, jj commit, jj log, and the concept of the working-copy-as-a-commit from previous chapters. Revsets build directly on these foundational ideas, allowing you to specify which commits these commands should operate on.
What Are Revsets and Why Do They Matter?
The Challenge of Referring to Commits
In traditional Version Control Systems (VCS) like Git, you often refer to commits using their SHA-1 hashes, branch names, or relative references like HEAD~3. While functional, these methods can become cumbersome, especially when dealing with complex histories or when you need to select commits based on various criteria (e.g., by author, message, or file changes).
jj’s mutable history model, where commits are frequently rewritten, amended, and rebased, further amplifies this challenge. Commit hashes change, and traditional branch names might not always capture the fluidity of your development. You need a dynamic way to point to “this commit, or its parent, or all commits by me that changed this file.”
Introducing Revsets: A Query Language for Commits
Revsets are jj’s elegant solution. They provide a concise yet powerful query language that allows you to:
- Select individual commits: By ID, by special keywords, or by their relationship to other commits.
- Filter commits: Based on properties like author, commit message, or changed files.
- Combine commit sets: Using logical operations like union, intersection, and difference.
- Specify ranges: To operate on sequences of commits.
Why does this matter? Revsets are the backbone of almost every advanced jj operation. Whether you’re rebasing a stack of changes, squashing commits, inspecting history, resolving conflicts, or synchronizing with a Git remote, you’ll use Revsets to tell jj which commits you want to operate on. They empower you to precisely target your actions, making complex history manipulations feel natural and intuitive.
๐ Key Idea: Revsets are jj’s flexible commit query language, essential for navigating and manipulating its mutable history. They provide a powerful way to describe “sets of commits” for any jj command.
Navigating Your History: Basic Revset Syntax
Let’s get hands-on and explore the most common and fundamental Revset expressions. We’ll use jj log -r <revset> to see what commits a Revset resolves to.
First, let’s set up a small repository with a few commits. This will give us a tangible history to query. We’ll assume you have a working development environment as of 2026-05-19.
# Start fresh: Remove any old repo named 'my-revset-repo'
rm -rf my-revset-repo
# Initialize a new jj repository
jj init my-revset-repo
cd my-revset-repo
# Configure author for consistent commit history
jj config set user.name "AI Expert"
jj config set user.email "ai@example.com"
# Create a first commit
echo "Hello, Revsets!" > file1.txt
jj commit -m "feat: initial commit"
# Create a second commit
echo "Adding another line to file1." >> file1.txt
echo "This is a new file." > file2.txt
jj commit -m "feat: add second line and file2"
# Create a third commit (this will be our working copy's parent)
echo "And a third line to file1." >> file1.txt
jj commit -m "feat: add third line to file1"
# Add some uncommitted changes to see the difference between . and @
echo "Uncommitted change." >> file1.txt
Now that our history is set, let’s explore! Note that commit IDs in your output will differ from the examples.
1. The Working Copy and Current Commit
These are your immediate starting points, representing where you are right now.
.(Dot): The Working Copy Commit The.(dot) Revset refers to the working copy commit. Injj, your working directory is always treated as a commit, even before you explicitlyjj commit. This output shows the commit that would be created if you ranjj commitright now, including any uncommitted changes.jj log -r .You should see output similar to this (commit ID and summary will vary):
@ 4f83a73c09e3 feat: add third line to file1 (uncommitted changes)Notice the
(uncommitted changes)part. This tells you that.includes the current state of your working directory.@(At Symbol): The Current Commit (Parent of Working Copy) The@(at symbol) Revset refers to the current commit. This is the parent of your working copy commit (the commit your working directory is currently based on). If you have uncommitted changes,@refers to the commit before those changes. If you have no uncommitted changes,.and@will refer to the same commit.jj log -r @Output:
@ 4f83a73c09e3 feat: add third line to file1Here, the
(uncommitted changes)is gone because@represents the committed state your working directory is based on, not including your current edits.๐ง Important:
.includes uncommitted changes;@refers to the last committed state your working directory is based on. This distinction is critical for operations that should or shouldn’t involve your ephemeral work.
2. Ancestors and Descendants: Navigating the Family Tree
Once you have a reference to a commit, you can navigate its family tree.
-(Minus): Parent Commit The-(minus) operator gives you the parent of the specified commit. So,@-means “the parent of the current commit”.jj log -r '@-'Output (your IDs will differ):
o 3f1a0e91005b feat: add second line and file2--(Double Minus): Grandparent Commit You can chain-operators to go further back in history.@--means “the grandparent of the current commit”.jj log -r '@--'Output:
o a2b1c3d4e5f6 feat: initial commitroot(): The Initial Empty Commitroot()refers to the initial, empty commit of the repository. Everyjjrepository starts with this special commit, which has no parent. It’s the ultimate ancestor.jj log -r 'root()'Output:
o 000000000000 (empty)A::B: Inclusive Range of Commits The::(double colon) operator specifies a range of commits, including ancestors and descendants betweenAandB(inclusive).root()::@means “all commits from the root commit up to and including the current commit”. This is a powerful way to visualize a linear history segment.jj log -r 'root()::@'This will show your entire history from the
root()commit up to your current commit@.@ 4f83a73c09e3 feat: add third line to file1 o 3f1a0e91005b feat: add second line and file2 o a2b1c3d4e5f6 feat: initial commit o 000000000000 (empty)A..B: Ancestor-Exclusive Range of Commits The..(double dot) operator means “the set of commits that are ancestors ofBbut not ancestors ofA.” This is often used to get the commits between two points, effectively excluding the starting point and its ancestors.Let’s use our history. Find the short ID of your
feat: initial commit. We’ll usea2b1c3d4e5f6as a placeholder.# Replace 'a2b1c3d4e5f6' with your actual initial commit ID jj log -r 'a2b1c3d4e5f6..@'This command means: “Show all commits that are ancestors of
@(current commit) but are not ancestors ofa2b1c3d4e5f6(initial commit).” The output would include your second and third commits:@ 4f83a73c09e3 feat: add third line to file1 o 3f1a0e91005b feat: add second line and file2It effectively selects all commits after
a2b1c3d4e5f6up to and including@.โก Quick Note: The
A..Bsyntax can be tricky. A simpler way to understand it is that it selects all commits on the path fromAtoB, excludingAitself and its ancestors. It’s useful for getting a range of commits that are descendants ofAbut ancestors ofB. For simple inclusive linear ranges,A::Bis often more intuitive.
3. Filtering by Commit Properties
Revsets allow you to filter commits based on their metadata.
author("pattern"): Filter by Authorauthor("pattern")selects commits whose author name or email matches the given pattern (case-insensitive substring match). You can use regular expressions here.jj log -r 'author("AI Expert")'This will show all commits by the “AI Expert” we configured.
@ 4f83a73c09e3 feat: add third line to file1 o 3f1a0e91005b feat: add second line and file2 o a2b1c3d4e5f6 feat: initial commitdescription("pattern"): Filter by Commit Messagedescription("pattern")selects commits whose commit message matches the pattern.jj log -r 'description("line")'This will show commits whose descriptions contain “line”.
@ 4f83a73c09e3 feat: add third line to file1 o 3f1a0e91005b feat: add second line and file2file("path"): Filter by Changed Filesfile("path")selects commits that changed the specified file. The path is relative to the repository root.jj log -r 'file("file2.txt")'This will show only the commit where
file2.txtwas introduced.o 3f1a0e91005b feat: add second line and file2
4. Combining Revsets: Set Operations
The real power of Revsets comes from combining them using logical set operations. Think of each Revset expression as defining a “set” of commits.
A | B(Union): Commits in A OR B.A & B(Intersection): Commits in A AND B.A - B(Difference): Commits in A BUT NOT B.~A(Complement): All commits not in A. (Use with caution, as “all commits” can be a very large set!)
Let’s put these into practice:
Intersection (
&): Commits matching BOTH criteria Let’s find all commits that touchedfile1.txtand had “line” in their description:jj log -r 'file("file1.txt") & description("line")'This should return the second and third commits, as they both modified
file1.txtand had “line” in their message.@ 4f83a73c09e3 feat: add third line to file1 o 3f1a0e91005b feat: add second line and file2Difference (
-): Commits in A, but NOT in B Now, let’s find all commits by “AI Expert” except for the initial commit. First, find your initial commit’s short ID (e.g.,a2b1c3d4e5f6).# Replace 'a2b1c3d4e5f6' with the actual short ID of your initial commit jj log -r 'author("AI Expert") - a2b1c3d4e5f6'This should show only the second and third commits, as the initial commit is subtracted from the set of all commits by “AI Expert”.
5. Special Keywords for Common Scenarios
jj provides several keywords for common commit selections, offering shortcuts for powerful queries.
heads(): All “Head” Commitsheads()selects all commits that are “heads” (meaning they have no children, or all their children are hidden). In a linear history, this is usually your current commit. In a more complex graph with multiple lines of development, it would show the tips of all those lines.jj log -r 'heads()'This will typically show your current commit (
@) and any other independent lines of development (e.g., if you had created a new branch and committed there).latest(N): The N Most Recent Visible Commitslatest(N)selects the N most recent visible commits. This is useful for quickly looking at your very recent history.jj log -r 'latest(2)'This will show the two most recent commits, including the working copy if it has changes (as
.is implicitly a “head”).public_commits(): Commits Safe for Public Sharingpublic_commits()selects commits that have been pushed to a remote or are ancestors of such commits. This is crucial for understanding what’s safe to rewrite without affecting collaborators, asjjgenerally recommends not rewriting public history.jj log -r 'public_commits()'Initially, in a new local repository, this will likely only show the
root()commit as nothing has been pushed yet.
This is just a glimpse! The jj official documentation provides a comprehensive list of Revset functions and operators, which you can explore as you become more comfortable.
Applying Revsets: Practical Examples
Revsets aren’t just for jj log. They are integrated into almost every jj command, allowing you to specify exactly which commits an operation should act upon.
Scenario 1: Rebase a Specific Range of Commits
Imagine you have a stack of three commits, A, B, and C (where C is the current commit @). You’ve decided that commits A and B need to be rebased onto an older, stable commit D to incorporate some foundational changes.
First, let’s simulate this by creating a new D commit further back in history.
(We’ll reset our working directory for this example to simplify things temporarily).
# Undo uncommitted changes for a clean state
jj restore --staged --from @
jj clean
# Go back to the initial commit's parent (the root)
jj new root()
echo "Old base content." > base_file.txt
jj commit -m "feat: stable base D"
# Now go back to our third commit to continue work
jj checkout @-
Now, imagine D is the feat: stable base D commit you just created. And A is our feat: initial commit. We want to rebase all commits from A through @ onto D.
# First, find the short ID of your "feat: initial commit" (let's call it A_ID)
# And the short ID of your "feat: stable base D" (let's call it D_ID)
# Example:
# A_ID = a2b1c3d4e5f6 (your initial commit)
# D_ID = e1f2g3h4i5j6 (your stable base D commit)
# Rebase commits from A_ID (inclusive) up to the current commit (@) onto D_ID
jj rebase -s 'A_ID::@' -d D_ID
This command tells jj to take the set of commits starting from A_ID up to and including @, and reapply them on top of D_ID. This is a powerful way to restructure your history precisely.
Scenario 2: Diffing Changes Between Two Arbitrary Points
You want to see all changes introduced between your feat: initial commit (let’s use a2b1c3d4e5f6 again) and your current commit (@). This is useful for code reviews or debugging.
# Replace 'a2b1c3d4e5f6' with your actual initial commit ID
jj diff -r 'a2b1c3d4e5f6::@'
This command shows the combined diff of all changes from a2b1c3d4e5f6 up to @. It’s like asking, “What’s the difference in state between these two points in history, considering all commits in between?”
Scenario 3: Finding Commits that Touched a File Before a Specific Point
You’re debugging an issue and suspect a change to file1.txt introduced it, but you only care about changes before a specific commit, say bugfix_commit_id (which you might have found from jj log).
First, let’s create a hypothetical bugfix_commit_id by making a new commit.
echo "Fixing a bug." >> file1.txt
jj commit -m "fix: temporary bugfix for example"
# Let's say the ID of this commit is 777777777777
# Now check out back to our original @
jj checkout @-
Now, suppose 777777777777 is our bugfix_commit_id. We want to find commits that changed file1.txt and are ancestors of this bugfix commit.
# Replace '777777777777' with the actual ID of your 'fix: temporary bugfix for example' commit
jj log -r 'file("file1.txt") & ancestors(777777777777)'
This Revset combines two filters: file("file1.txt") to get commits that changed the file, and ancestors(777777777777) to limit those commits to ones that are ancestors of the bugfix_commit_id. The & operator ensures both conditions are met. This query helps narrow down the search for the root cause.
Mini-Challenge: Advanced Commit Discovery
Your turn! Let’s put your Revset knowledge to the test.
Challenge: Find all commits authored by “AI Expert” that are descendants of the feat: initial commit (use its ID, e.g., a2b1c3d4e5f6) and contain the word “file1” in their description, but exclude the current working copy commit (.).
Hint: You’ll need to combine author(), description(), descendants(), and the difference operator -. Remember that . includes uncommitted changes.
What to observe/learn: This challenge requires combining multiple Revset clauses to form a precise query, demonstrating the flexibility and power of the language. It also reinforces the distinction between the working copy and other commits, and how to exclude specific commits.
Solution (click to expand)
First, make sure you’re back in the my-revset-repo directory and have your initial commits set up.
Find the actual short ID for your feat: initial commit. Let’s assume it’s a2b1c3d4e5f6 for this example.
# Replace 'a2b1c3d4e5f6' with your actual initial commit ID
jj log -r 'author("AI Expert") & description("file1") & descendants(a2b1c3d4e5f6) - .'
This command should output only the “feat: add third line to file1” commit. The “feat: add second line and file2” commit might be excluded if its description doesn’t explicitly mention “file1” (it mentions “file2”). If it does mention “file1”, it would be included. The key is that it correctly filters by all specified criteria and excludes the working copy.
Common Pitfalls & Troubleshooting
Confusing
.and@:๐ Key Idea:.is the working copy commit (including uncommitted changes).๐ Key Idea:@is the current commit (its parent, representing the last committed state).- For operations that modify history (e.g.,
jj rebase), you often want to target@or its ancestors. For operations that inspect the current state (e.g.,jj diff),.is useful if you want to include your ephemeral work.
A::Bvs.A..B:A::B(inclusive range): “All commits that are descendants ofAAND ancestors ofB(inclusive).” This is often what you want for a linear range of commits.A..B(ancestor-exclusive range): “The set of commits that are ancestors ofBbut not ancestors ofA.” This selects commits betweenAandB, excludingAand its ancestors. It’s less intuitive for simple linear ranges. When in doubt for inclusive ranges, stick withA::B.
Over-complicating Queries: Start simple. If you need to build a complex Revset, try breaking it down into smaller parts and testing each part with
jj log -r 'part'before combining them. This modular approach helps debug your Revset.Forgetting to Quote: If your Revset contains spaces or special characters (like
|,&,(,)), always wrap the entire expression in single quotes'...'to prevent your shell from interpreting them. Forgetting quotes is a very common source of “command not found” or “invalid argument” errors.Testing Your Revsets: Before running a destructive command like
jj rebaseorjj squashwith a complex Revset, always test it withjj log -r '<your_revset>'to ensure it selects exactly the commits you intend. This simple step can save you from unintended history rewrites.
Summary
In this chapter, we’ve embarked on a journey into Revsets, jj’s powerful commit query language. We covered:
- The necessity of Revsets for navigating and manipulating
jj’s mutable history with precision. - Basic Revset expressions like
.(working copy),@(current commit), androot()for identifying key points in your history. - Navigating history with operators like
-(parent) and::(inclusive range), and..(ancestor-exclusive range) to select sequences of commits. - Filtering commits by properties like
author(),description(), andfile()to narrow down your search. - Combining Revsets using set operations:
|(union),&(intersection), and-(difference) for complex queries. - Practical examples of using Revsets in
jj rebaseandjj diffcommands, demonstrating their utility in real-world workflows.
Revsets are a fundamental tool in your jj arsenal, enabling you to interact with your codebase’s history with unmatched precision. As you become more proficient, you’ll find yourself crafting sophisticated queries to streamline your development and debugging workflows.
Next, we’ll build upon this foundation by exploring jj’s core philosophy of stacked changes and how Revsets play a crucial role in managing these incremental units of work, leading to cleaner, more reviewable code.
References
- Jujutsu Official Documentation: Revsets
- Jujutsu Official Documentation: Tutorial
- Jujutsu GitHub Repository
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.