Figuring out where our changes are with revsets
We have learned about two kinds of identifiers in jj
: change IDs and commit
IDs. But what if we want to talk about, for example, a range of commits?
jj
has a concept called a "revset," short for "revision set." Sometimes
people say "revision" instead of "commit," and "revset" is just nicer to say
than "comset", so it stuck.
This sounds pretty intense at first, but I promise it's simpler than you
think: jj
supports a functional language to describe revsets. Almost
every command in jj
takes a -r
/--revision
flag, which is the revision
to operate on. This defaults to @
. This means when we do jj new
, we're
basically doing jj new -r @
, that is, create a new change with a parent
revision of the current working copy.
Symbols
@
is actually our first example of the revset language. This is called a
"symbol". Symbols are a means of specifying a single commit. @
refers to
the change containing the current working copy, but a change ID or commit ID
are other examples of symbols.
Operators
Operators let you describe more complex relationships between changes. For
example, remember how in the squash workflow, we would move the contents of
the working directory into the parent change? Well, the -
operator refers to
the parent of a given revision, and @
is the change referring to the current
working directory, so we might say "we squashed the contents of @
into @-
.
And in fact, jj squash
is short for jj squash -r @
. There are many operators,
including, but not limited to:
x & y
: changes that are in both x and yx | y
: changes that are in either x or y::x
Ancestors of xx::
Descendants of x
And more. The final bit is the most interesting, and that's functions.
Functions
Functions allow for even more complex selection of a series of changes. The simplest functions are:
root()
: a function that returns the root changeall()
: this function returns all visible changesmine()
: this function returns all changes authored by the current user
More complex functions can take arguments:
parents(x)
: the parent changes ofx
ancestors(x)
: the same as::x
, but see the next exampleancestors(x, depth)
: limits the results to a certain depth, which you can't do with the::x
syntaxheads(x)
: commits inx
that are not ancestors of other commits inx
.description(x)
: commits that havex
in their description
Putting it all together
Now we can understand heads(all())
from before: these are two functions, where
we're asking for the head commits of every commit in the repository.
Revsets are very powerful, and very convenient. Would you like to find every commit by me containing the world "print" in the description? Try this:
$ jj log -r 'author("Steve Klabnik") & description(print)'
Another really useful revset function is trunk()
:
$ jj log -r 'trunk()'
◉ zzzzzzzz root() 00000000
Right now, this doesn't look very useful, but it will be more useful when we
get into sharing our changes. trunk()
looks for a remote named origin
or
upstream
, and looks for a main
, master
, or trunk
branch, and then
provides that. Since we don't have any of those right now, it gives us the same
as root()
.
Additionally, on the jj
Discord, several folks have settled on this as a
decent revset for larger repositories:
$ jj log -r '@ | ancestors(remote_bookmarks().., 2) | trunk()'
This will show the history from the working directory, some detail about remote branches, as well as the trunk. What's good varies between what you're trying to do and what your repository looks like, so experiment with some of this stuff to find something that works well for you.
Revsets are very powerful, and you'll learn some useful ones as you explore more. At some point, we'll even talk about how to create custom aliases for revsets, but for now, let's get back to dealing with branches and how to merge them.