In short: jj picks the remotes. For each one, it collects ref updates from the flags, or by default
from the revsets.git-push revset (remote_bookmarks(remote=…)..@).
--change and --named first create local bookmarks. Each candidate ref is
classified against its tracked remote ref: no change, conflicted, untracked, or an update. Every
commit between the remote's known heads and the new targets is validated: it needs a description, an
author and committer, no conflicts, and it can't be private. If git.sign-on-push is
set, your unsigned mutable commits are signed and recorded as their own operation first. jj prints Changes to push to <remote>:, runs
git push --porcelain --no-verify --force-with-lease=…, updates the pushed remote refs,
and records push … to git remote ….
The command is cmd_git_push. The decision for each
ref is classify_ref_push_action in jj-lib. The push
itself is push_refs. Remote bookmarks and their
tracking state live in the view (view page). The
inverse command is jj git fetch.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
--remote REMOTE | name patterns, repeatable | git.push, else the only remote, else origin | Remotes to push to, globs by default (source). |
-b, --bookmark | name patterns, repeatable | none | Push matching bookmarks. Creating a new remote bookmark is allowed if the name isn't tracked on any remote yet, and deleting is allowed because the name was given explicitly (source, params). |
-t, --tag | name patterns, repeatable | none | Push matching tags, with the same rules (source). |
--all | bool | off | Every bookmark and tag, including new ones (source). |
--tracked | bool | off | Every tracked bookmark and tag (source). |
--deleted | bool | off | Push deletions of bookmarks and tags deleted locally. With --all/--tracked, it allows deletions in that set (source). |
-r, --revisions REVSETS | revsets, repeatable | none | Push bookmarks and tags pointing at these commits (source). |
-c, --change REVSETS | revsets, repeatable | none | Create a bookmark named by templates.git_push_bookmark on each commit, then push it (source). |
--named NAME=REVISION | repeatable | none | Create a new bookmark NAME on one commit and push it (source). |
--allow-empty-description, --allow-private, --allow-conflicts | bool | off | Skip one of the commit checks (source). |
--dry-run | bool | off | Print the changes and stop (source). |
-o, --option | string, repeatable | none | Passed to Git as --push-option (source). |
Two clap groups shape the flags. --bookmark, --tag, -r,
--change and --named form the "specific" group, and --all and
--tracked form a "what" group that conflicts with it (source).
The global --no-integrate-operation is rejected with a hint to use --dry-run
(source).
2. Configuration
| Key | Default | Effect |
|---|---|---|
git.push | unset | Remote name pattern, or a list of them, used when --remote isn't given (source, docs). |
revsets.git-push | remote_bookmarks(remote=exact:remote)..@ | The default selection, with remote bound to each remote's name (default, evaluation). |
templates.git_push_bookmark | "push-" ++ change_id.short() | Names the bookmarks --change creates (default). |
git.private-commits | none() | A revset of commits that must not be pushed (default). |
git.sign-on-push | false | Sign unsigned mutable commits authored by you (author email equal to user.email) before pushing, whatever signing.behavior says (source, filter, should_sign). |
revset-aliases."immutable_heads()" | (built-in) | Immutable commits are treated as already validated, and never signed. |
git.executable-path | git | The Git binary jj runs. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
View.local_bookmarks, local_tags | read, maybe write | The local side of every comparison. --change and --named add new bookmarks (create_change_bookmarks, named). | view |
View.remote_views | read, write | The tracked remote target is both the comparison base and the lease. Pushed refs become Tracked at their new target (source). | view |
Git refs refs/jj/remote-* | write | Pushed bookmarks update jj's private fetch refs under refs/jj/remote-bookmarks/, and pushed tags update refs/jj/remote-tags/; a failure there is only logged (source, to_fetched_ref_update). Git's own refs/remotes/<remote>/* are left alone: the subprocess is given a negative fetch refspec so git push doesn't update them (source). In a non-colocated workspace, jj git export writes them. | view |
| Commits | read, maybe rewrite | Validated. With git.sign-on-push, your unsigned mutable commits are rewritten with signatures and their descendants rebased, and the ref updates are remapped to the new IDs (sign_commits_before_push). If anything was signed, the signatures are committed before the push starts (source). | commits |
| Operation | write | One operation if anything was pushed, preceded by a sign commits before push operation if commits were signed. is_push_operation recognises it by its push prefix (source). | operations |
5. Transaction and operation
- Transaction: one, started right after the remotes are chosen (source). Created bookmarks and updated remote refs go into it.
- Signing: if
git.sign-on-pushsigned anything, the bookmarks--change/--namedcreated are taken out, the transaction is committed assign commits before push, and a new transaction is started with those bookmarks put back (source). A failed push keeps the signatures but still drops the created bookmarks; if nothing needed signing, there is no extra operation (docs). - When it is committed: only if every push succeeded or at least one ref was pushed (source).
--dry-runand "Nothing changed." return without committing, so bookmarks created by--changeare dropped. The source notes that on partial success those bookmarks are kept. - Operation description:
push all bookmarks/tags to …,push all tracked bookmarks/tags to …,push all deleted bookmarks/tags to …, orpush bookmark X/push bookmarks X, Y/push tag Tfollowed byto git remote R(orto git remotes R1, R2) (make_updates_term, source). - Working copy: untouched, unless signing rebased
@. jj undorestores the old remote-tracking state locally. It doesn't unpush anything from the remote, and it doesn't undo the signatures, which are a separate operation.
6. Algorithm and invariants
Classifying a ref
classify_ref_push_action compares the local target
with the remote ref's tracked target, which is absent when the remote ref is untracked:
- Equal: nothing to push.
- Local conflicted, or remote conflicted: rejected.
- Remote present but not tracked: rejected (
Non-tracking remote bookmark … exists). - Otherwise,
Update(before = remote, after = local), where either side may be absent.
The command then gates updates (classify_bookmark_update).
Creating an untracked remote ref needs allow_new, and a deletion needs
allow_delete. The default and -r paths set neither, so they only update refs already tracked
on that remote. That includes a tracked name the remote doesn't have yet, which is created. --bookmark allows creation only for
names tracked nowhere (source).
Force-with-lease
Every update is a forced refspec, guarded by a lease on the expected remote value, the
before side. An empty lease means "must not exist yet" (push_updates,
RefToPush::to_git_lease). Git ignores leases on
+-prefixed refspecs, so the refspecs are passed unforced
(spawn_push). --no-verify skips Git
hooks. If the remote ref moved since the last fetch, Git rejects it and jj reports it under
The following references unexpectedly moved on the remote
(source). This is how jj avoids overwriting someone else's push
without a separate fetch.
Commit validation
validate_commits walks
(known remote heads | immutable_heads())..new_heads, where the known heads are the targets
of the remote's bookmarks and tags, so a commit already on the remote as a tag isn't re-checked
(source). It rejects the first commit that
has an empty description, a missing author or committer name or email, a conflict, or that is
private. Validated heads are added to the known heads, so later refs don't re-check shared ancestry.
Refs named explicitly fail the whole command. Refs picked up by the default revset or -r
are skipped with Won't push bookmark … (source).
Invariants: the remote is only changed where its current value equals jj's last recorded one. Untracked remote refs are never overwritten. Conflicted bookmarks are never pushed. The default selection never deletes a remote ref, and never creates one for a name that isn't tracked.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No git remotes to push to | The remote selection matched nothing. | push.rs |
Nothing changed. | No ref needs updating on any remote. | push.rs |
Bookmark <name> is conflicted / Bookmark <name>@<remote> is conflicted | The local or remote target has several sides. | push.rs |
Non-tracking remote bookmark <name>@<remote> exists | Pushing over an untracked remote bookmark. The hint suggests jj bookmark track. | push.rs |
Refusing to create new remote bookmark <name>@<remote> | Creating a remote bookmark from a path that doesn't allow new ones. | push.rs |
Refusing to push deleted bookmark <name> | A deletion without --deleted or an explicit name. | push.rs |
Won't push commit <id> since it has no description (or has conflicts, is private, has no author and/or committer set) | An explicitly selected ref would send a failing commit. | push.rs |
Bookmark already exists: <name> | --named, or a --change name already in use elsewhere. | push.rs |
Argument '…' must include '=' and have the form NAME=REVISION | A malformed --named value. | push.rs |
Warning: No bookmarks/tags found in revsets.git-push: … (remote=…) | The default revset selected no refs, for example after moving a bookmark backwards. | push.rs |
The following references unexpectedly moved on the remote: … (reason: stale info) | The lease failed. The command ends with Failed to push some bookmarks. | git_util.rs, push.rs |
The remote rejected the following updates: | The server refused, for example a protected branch. | git_util.rs |
--no-integrate-operation is not respected; use --dry-run | That global flag was passed. | push.rs |
8. Worked example
From upstream's test_git_push_current_bookmark.
bookmark2@origin is on 38a20473. Locally, bookmark2 and a new
my-bookmark are on its child 88ca14a7, which is @.
bookmark1 was rewritten to e5ce6d9a on a side branch. Then
jj git push uses the default revset remote_bookmarks(remote=origin)..@:
Upstream's snapshot:
$ jj git push
Changes to push to origin:
bookmark: bookmark2 [move forward from 38a204733702 to 88ca14a7d46f]
bookmark: my-bookmark [add to 88ca14a7d46f]
my-bookmark is new on the remote, but the setup configures
auto-track-bookmarks = '*', so it was already tracked and counts as an update. Later in
the same test, after bookmark2 is moved back to its parent, plain
jj git push finds nothing (No bookmarks/tags found in revsets.git-push),
and jj git push -b bookmark2 pushes
[move backward from 88ca14a7d46f to 38a204733702]
(source). If someone else had moved the bookmark on the remote,
the lease would fail instead, as in
test_git_push_forward_unexpectedly_moved.