In short: jj resolves the revisions (default @) and checks they are mutable. If the set is
empty it prints No revisions to modify. and stops. In one transaction it runs
transform_descendants over the set. For each target it builds a rewrite and applies the
requested author, timestamp, description and change-ID changes. It writes a new commit only if one of
them differs from the old commit, or --force-rewrite is set. Descendants are reparented.
The operation is edit commit metadata for commit <id>, plus
and N more when there are several.
The command is cmd_metaedit. To edit a description
in an editor, use jj describe. What a commit's metadata fields are, and how a rewrite copies them, is
on the commits page.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSETS (positional), -r (hidden alias) | revsets, repeatable | @ | The commits to modify. Both forms are unioned (source). |
--update-change-id | bool | off | Give each commit a new random change ID (source). |
-m, --message | string, repeatable | none | Set the description. Several values become paragraphs. No editor is opened (source). |
--update-author-timestamp | bool | off | Set the author date to now (the new committer timestamp) (source). |
--update-author | bool | off | Set the author name and email to the configured user. The author date is kept (source). |
--author "Name <email>" | author string | none | Set the author name and email explicitly. Conflicts with --update-author (source). |
--author-timestamp DATE | RFC 2822 or RFC 3339 date | none | Set the author date. Conflicts with --update-author-timestamp (source). |
--force-rewrite | bool | off | Rewrite every target even if nothing else changed. This refreshes the committer (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
user.name, user.email (or JJ_USER, JJ_EMAIL) | unset | The committer of every rewritten commit, and the author for --update-author (help, for_rewrite_from). |
revset-aliases."immutable_heads()" | (built-in) | Immutable targets are refused by check_rewritable_expr. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Commit metadata (author, committer, description, change ID) | read, write | New commits copy the old tree and parents (remapped), with changed metadata (for_rewrite_from). | commits |
| Tree | read | Never changed. Descendants keep their own trees too. | trees |
| Rewrite records and predecessors | write | Each rewritten commit records its old version as predecessor, so jj evolog shows the metadata edit. | transactions |
Bookmarks, @ | write | Moved to the rewritten commits when the transaction finishes. | view |
| Operation | write | One operation, or none if nothing changed. | operations |
5. Transaction and operation
- Transaction: one, after the rewritability check (source).
- Operation description:
edit commit metadata for commit <full hex id>, or… for commit <id> and <N> more(source). The ID is the first target's old commit ID. Upstream's evolog showsedit commit metadata for commit 75591b1896b4990e7695701fd7cdbb32dba3ff50(test). - Commits rewritten: targets with a real change, and descendants whose parents moved.
- No-op runs: when nothing differs, no commit is written and the transaction finishes with
Nothing changed.(test).
6. Algorithm and invariants
The source comment explains why
transform_descendants is used instead of rewriting each target: if one target descends
from another, it is still rewritten only once, so its predecessor chain gets one entry, not two.
The builder comes from rewriter.reparent(), which already applies two defaults from
for_rewrite_from. First, the committer becomes the
current user at the current time. Second, an empty author name or email is filled in from the
committer, and a discardable commit authored by the committer gets its author date reset. So the
change check compares against the old commit's author, not the builder's
(source). A timestamp counts as changed only if it differs from
both the builder's and the old commit's. That is why repeating the same --author-timestamp
is a no-op, as upstream's test_metaedit_set_same_timestamp_twice
checks.
Precedence (source): --author wins over
--update-author (clap also rejects using both). --author-timestamp is applied
after --update-author-timestamp, and clap rejects that pair too.
Invariants: trees never change. Immutable commits are never rewritten. Without
--force-rewrite, a target is rewritten only if its metadata changes or its parents were
rewritten.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions to modify. | The revsets resolve to nothing. This is a status message, not an error. | metaedit.rs, test |
Nothing changed. | No option changed anything, for example a bare jj metaedit, or --update-author when you are already the author. | test |
Commit … is immutable | A target is in immutable_heads()::. | metaedit.rs (CLI page) |
error: the argument '--author <AUTHOR>' cannot be used with '--update-author' | Both author options were given. Clap exits with status 2. | test |
clap: invalid value for --author / --author-timestamp | Not of the form Name <email>, or an unparseable date. | metaedit.rs |
8. Worked example
From upstream's test_new_change_id. A stack a →
b → c, where c is @. Then
jj metaedit --update-change-id kkmpptxzrspx gives b a new change ID:
Upstream's snapshot:
$ jj metaedit --update-change-id kkmpptxzrspx
Modified 1 commits:
yqosqzyt 01d6741e b | (no description set)
Rebased 1 descendant commits.
Working copy (@) now at: mzvwutvl 0c3fe2d8 c | (no description set)
Parent commit (@-) : yqosqzyt 01d6741e b | (no description set)
The evolog for the new change ID yqosqzyt lists the old kkmpptxz commits as
its predecessors, linked by the
edit commit metadata for commit 75591b18… operation (test).
So the history isn't lost, even though the change ID is new.