In short: jj resolves the revisions (default @), refuses if any is immutable, and builds
the new descriptions. With one commit and no -m/--stdin, it opens one file
in your editor. With several, it opens one file with a JJ: describe <id> section
per commit. Commits whose description didn't change are dropped. The rest go through
transform_descendants, which writes each described commit and reparents every
descendant. Everything is one operation, describe commit <id>. If nothing
changed, no operation is recorded at all.
The command is cmd_describe. Building and parsing
the editor text lives in description_util.rs, which
jj new, commit,
squash and others share. The default alias desc comes from
misc.toml.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSETS (positional) | revsets, repeatable | @ | The commits to describe. All positional and -r values are unioned (source). |
-r REVSETS | revsets, repeatable | none | Hidden alias for the positional argument, kept for symmetry with other commands (source). |
-m, --message MESSAGE | string, repeatable | none | Use this description and don't open the editor. Several -m values become paragraphs: each gets a trailing newline, and they are joined with a blank line (join_message_paragraphs). The same text goes on every target. Conflicts with --stdin (source). |
--stdin | bool | false | Read the description from standard input. A trailing newline is added if missing (source, read). |
--editor | bool | false | Open the editor even with -m or --stdin, pre-filled with that text (source). |
The editor opens when --editor is given or no description was supplied:
use_editor = args.editor || shared_description.is_none()
(line 151). At this pin, describe has no flags for
the author or for trailers. Authorship is handled elsewhere, as described below.
2. Configuration
| Key | Default | Effect |
|---|---|---|
ui.editor |
nano on Unix (unix.toml), Notepad on Windows (windows.toml) |
The editor command (TextEditor::from_settings). It may use
$path and $line; without $path, the file path is appended. Environment
variables fit into the config layers: $VISUAL, then $EDITOR, sit below your
config files (source), while $JJ_EDITOR
sits above them (source). Layer order:
ConfigSource.
|
templates.draft_commit_description |
builtin_draft_commit_description (source) |
Renders the text the editor starts with (description_template).
The built-in version (source) shows the description, or
default_commit_description (empty, source) if there is none,
then JJ: Change ID: … and a JJ:-indented diff.summary().
builtin_draft_commit_description_with_diff adds JJ: ignore-rest and the full Git diff
(source).
|
templates.commit_trailers |
empty (source) |
If set, a commit template whose output lines are trailers (for example Signed-off-by: …) to
append to each description (parse_trailers_template).
|
user.name, user.email |
unset | Become the rewritten commit's committer, and fill in an empty author name or email (source). |
revset-aliases."immutable_heads()" |
see the CLI page | Decides which targets check_rewritable_expr rejects. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commits | read, then rewritten | Each changed target gets a new commit: same tree, parents (possibly rewritten), change ID and author, a new description, and a new committer signature. The old commit is recorded as its predecessor. | commits |
| Descendant commits | rewritten | Reparented onto the rewritten parents with CommitRewriter::reparent, which keeps their tree as is (source). | transactions |
| Temporary commits | written, hidden | write_hidden stores a commit so templates and trailers can be rendered against it, without adding it to the view (source). Nothing references these commits afterwards. | commits |
View.head_ids | write | Rewritten commits replace the old ones as heads. | view |
View.local_bookmarks | write | Bookmarks on a rewritten commit move to its new version (update_rewritten_references). | view |
View.wc_commit_ids | write | Any workspace whose @ was rewritten now points at the new commit. | view |
| Operation | write | One operation, described below, holding the predecessor records. | operations |
Working copy (TreeState) | read, then maybe write | Snapshotted when the workspace loads. If @ was rewritten, the lifecycle checks out the new @. Its tree is unchanged, so no file contents change. | working-copy |
5. Transaction and operation
- Transaction: one, started after the revisions are resolved and checked (source).
-
Operation description (source):
describe commit <id>for one commit, ordescribe commit <id> and <n> morefor several.<id>is the full hex ID of the first commit in evaluation order, which is the newest, since revsets evaluate children first. It is computed from all resolved targets, including any whose description ends up unchanged. - Commits created or rewritten: one new commit per target whose description changed, plus one per descendant of those targets. Descendants are reparented, not rebased: their trees are kept, so describing can never introduce conflicts.
-
Nothing changed: if every new description equals the old one, no commit is
rewritten.
finishsees no view change, printsNothing changed.and records no operation (source). The hidden temporary commits don't count, because they never enter the view. - Working copy: snapshotted at the start, so the commits you describe include your latest edits. If
@was rewritten, the new@is checked out after the operation commits. jj undorestores the previous view: the old commits become visible again with their old descriptions, and the rewritten ones are hidden.
6. Algorithm and invariants
Building each description
-
Start from the existing description.
rewrite_commitcopies the stored commit (for_rewrite_from). It clears predecessors, sets the committer to you, now, and fills in an empty author name or email from the committer. If the old commit was discardable (empty, with no description) and you are its author, the author timestamp is reset to now as well..detach()turns it into aDetachedCommitBuilderthat holds the draft without touching the repo. - Apply
-mor--stdinto every builder, if given (source). -
Add trailers if
templates.commit_trailersis non-empty. They are skipped for a builder whose description is still empty and no editor will open. The source comment explains why: the first trailer would become the subject line, and an empty description is what lets an untouched working-copy commit be discarded (source). Trailers already present aren't repeated, and a new trailer paragraph is separated by a blank line (add_trailers_with_template). -
Edit, if
use_editor. For one commit,edit_descriptionappends a blank line andJJ: Lines starting with "JJ:" (like this one) will be removed., then writes a temporary.jjdescriptionfile. The cursor goes to the first line that isn't aJJ:comment. For several,edit_multiple_descriptionswrites one file. It has a header, then aJJ: describe <12-hex commit id> -------section per commit in topological order (parents first), each filled from the draft template. -
Clean up what the editor returns
(
cleanup_description_lines). Lines starting withJJ:are dropped, everything fromJJ: ignore-restonwards is dropped, and leading and trailing blank lines are trimmed. A non-empty result ends in exactly one newline.
Parsing the bulk file
parse_bulk_edit_message starts a new section at
each line beginning JJ: describe . It strips trailing whitespace and dashes from the rest
of that line to get the commit ID prefix. Before the first section, only blank lines and
JJ: lines are allowed. Each section is matched to a commit by its exact 12-character
short ID, and the IDs are then validated: any commit with no section, any section repeated, or any
unknown ID is an error, and nothing is written
(source). The order of sections in the file doesn't matter.
Rewriting
Unchanged descriptions are filtered out first, so descendants aren't rewritten for nothing
(source). The source comment explains why it then uses
transform_descendants rather than rewriting
each target and calling rebase_descendants. When one target descends from another, the
later approach would rewrite it twice and add an extra predecessor link. transform_descendants
visits each commit in the descendants of the targets exactly once, parents first
(transform_commits). It hands each one a
CommitRewriter whose new parents already account for rewrites made earlier in the walk
(source):
- a target:
reparent(), set the edited description,write(), and count it as described; - any other descendant:
reparent()andwrite(), and count it as rebased.
Note that the final commits come from a fresh rewrite_commit inside
reparent(). Only the description text is taken from the draft builders. Then bookmarks
and working-copy pointers move to the new commits. Updated N commits. is printed only when
N > 1, and Rebased N descendant commits. whenever a non-target was rewritten.
Invariants: trees never change, so no conflicts can arise; change IDs are kept, so every rewritten commit is the same change; and each commit is rewritten at most once per invocation.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions to describe. | Status message (not an error) when the revsets match nothing. No transaction is started. | describe.rs |
Commit … is immutable / The root commit … is immutable | Any target is immutable. Hints name the commit and say how many immutable commits would be rewritten. | cli_util.rs |
Nothing changed. | Every description came out the same as before, including a -m that differs only by the trailing newline. No operation is recorded. | cli_util.rs |
clap: --message cannot be used with --stdin | Both are given. | describe.rs |
Failed to edit description, caused by Failed to run editor '…' or Editor '…' exited with … | The editor can't be started or exits non-zero. A hint says Edited description is left in <path>. | description_util.rs, command_error.rs |
Found the following line without a commit header: "…" | Bulk file: text before the first JJ: describe line. | description_util.rs |
The description for the following commits were not found in the edited message: … | Bulk file: a commit's section was deleted. | description_util.rs |
The following commits were found in the edited message multiple times: … | Bulk file: a section appears twice. | description_util.rs |
The following commits were not being edited, but were found in the edited message: … | Bulk file: a section names an ID that isn't a target. | description_util.rs |
Trailers should be valid utf-8 | The trailer template rendered non-UTF-8 text. Output that doesn't parse as trailers fails with the trailer parser's error. | description_util.rs |
Edge cases: an empty -m "" clears the description, and makes an empty commit discardable
again. The bulk-file header warns that a syntax error loses your text. That's because the temporary
file is deleted as soon as the editor exits successfully, before the text is parsed
(edit_str). Describing the same commit through two
revsets is fine, since the targets are a set.
8. Worked example
This is upstream's own test (test_describe_multiple_commits).
It starts from a chain of three empty, undescribed commits on the root, with @ at the
top, and describes the top and the bottom in one go:
jj describe -r@ -r@-- -m "description from CLI".
The command prints, on stderr:
Updated 2 commits.
Rebased 1 descendant commits.
Working copy (@) now at: kkmpptxz 4c3ccb9d (empty) description from CLI
Parent commit (@-) : rlvkpnrz 650ac8f2 (empty) (no description set)
All three commits get new IDs, because each one's parent changed or its own description did. The
middle commit only got a new parent, so its description and tree are untouched. The operation is
describe commit 3cd3b246e098… and 1 more, naming the old @ in full.
Running the same command again prints Nothing changed. and records nothing. Running plain
jj describe -r@ @- instead opens a bulk file like this
(from the same test), with
the parent first:
JJ: Enter or edit commit descriptions after the `JJ: describe` lines.
JJ: Warning:
JJ: - The text you enter will be lost on a syntax error.
JJ: - The syntax of the separator lines may change in the future.
JJ:
JJ: describe 650ac8f249be -------
JJ: Change ID: rlvkpnrz
JJ:
JJ: describe 4c3ccb9d4fb2 -------
description from CLI
JJ: Change ID: kkmpptxz
JJ:
JJ: Lines starting with "JJ:" (like this one) will be removed.