Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
1# Pushing changes with jj
2
3How work gets from your working copy to a change on Dogfood.
4
5This repository is a **jj** repo with a git backend. `jj git export` keeps a git view in
6sync, which means `git status` and the git reflog see only part of the picture — the
7authoritative history is jj's, and `jj op log` is the thing that can undo a mistake.
8
9## The model, in one paragraph
10
11There is no staging area and no "uncommitted work". The working copy **is** a commit
12(`@`), and jj snapshots it into that commit before running any command. You do not
13create a commit to save work; the work is already in one. What you do instead is give
14that commit a description, and then push a bookmark pointing at it. Pushing a bookmark
15is what opens or updates a change — there is no web form and no PR button.
16
17## The normal loop
18
19```sh
20# 1. Start a new commit on top of the trunk.
21jj new main
22
23# 2. Edit files. jj snapshots them into @ automatically — nothing to add.
24
25# 3. Describe what you did.
26jj describe -m 'feat: cache shortest-unique prefixes per repo'
27
28# 4. Push. This creates a bookmark named push-<change-id> and opens a change.
29jj git push -c @
30
31# 5. Start the next piece of work on top.
32jj new
33```
34
35Step 4 prints the change URL. That change is now open for review.
36
37## Amending a change after review comments
38
39Because `@` is still the commit you pushed, you just keep editing it and push again:
40
41```sh
42# edit files …
43jj git push -b push-<change-id>
44```
45
46jj reports this as `[move sideways from <old> to <new>]`. "Sideways" means a rewrite
47rather than a fast-forward — expected, because you changed a commit that was already
48published. No `--force` is required: jj compares the remote against the position it last
49recorded, and pushes only when those agree. If someone else moved the bookmark
50underneath you, that check fails instead of clobbering their work.
51
52On Dogfood this lands as a **new revision on the same change**. The change id is stable
53across every amend, rebase and force-push, which is the whole point — reviews stay
54attached to the change, not to a commit hash.
55
56If you have already moved on with `jj new` and `@` is no longer the commit you want to
57amend:
58
59```sh
60jj edit <change-id> # make that commit the working copy again
61# edit files …
62jj git push -b push-<change-id>
63jj new # go back to working on top
64```
65
66## Checking before you push
67
68```sh
69jj st # what changed in @
70jj diff --stat # the diff, summarised
71jj log -r 'stack(@)' # the stack this change sits in
72jj git push -b <name> --dry-run # exactly what would move, without moving it
73```
74
75`--dry-run` is worth the extra command whenever you are about to rewrite something that
76is already published.
77
78## Naming the bookmark yourself
79
80`-c/--change` generates `push-<change-id>`, which is fine for most work. To push under a
81name a human chose:
82
83```sh
84jj bookmark create my-feature -r @
85jj git push -b my-feature
86```
87
88## Landing on main
89
90```sh
91jj bookmark move main --to @
92jj git push -b main
93```
94
95This bypasses review entirely, so it is for trivial or already-reviewed work only. The
96normal path is the bookmark push above.
97
98## The trap that will cost you a day
99
100```sh
101jj new main -m 'feat: my work' # ← DO NOT use this to save work in progress
102```
103
104This does **not** put your current work in the new commit. jj snapshots the working copy
105into the **current** commit first, then creates an **empty** commit whose parent is
106`main`. You end up pushing an empty change while all your work sits in a now-orphaned
107commit labelled `(no description set)` — and because the working copy was reset to
108something matching `main`, `git status` reports a clean tree and the work looks deleted.
109
110Use `jj describe -m '…'` to name work you already have. Use bare `jj new` (no revision)
111only when you genuinely want to start something new.
112
113### Recovering from it
114
115Nothing is lost — jj keeps every snapshot.
116
117```sh
118# 1. Find the stranded commit. It is a sibling with no description.
119jj log -r 'all()'
120
121# 2. Confirm it is the right one before touching anything.
122jj show --stat <change-id>
123
124# 3. Restore its contents into the current commit.
125jj restore --from <change-id>
126```
127
128`jj op log` shows the tell: a `snapshot working copy` operation immediately followed by
129`new empty commit`, both with the same `args:` line. To rewind the whole repository to
130just before a bad command:
131
132```sh
133jj op log
134jj op restore <operation-id>
135```
136
137Prefer `jj restore --from` when you only want the files back and do not want to rewind
138bookmarks that were pushed in the meantime.
139
140## Deploying is a separate step
141
142Pushing updates the change. It does not deploy. `./run.sh deploy` builds from the
143**working copy on disk**, not from what you pushed — so if the tree is in the stranded
144state above, a deploy will quietly ship the previous build. After deploying, confirm
145which build is actually live rather than assuming:
146
147```sh
148curl -s -o /dev/null -w '%{http_code}\n' https://dogfood.sh/assets/terminal.js
149```
150
151Any asset that exists only in the current build works as the probe.
152
153## See also
154
155- [`change-id-format.md`](change-id-format.md) — why change ids look the way they do
156- [`revset-semantics.md`](revset-semantics.md) — the revset language used by `-r`

156 lines · Markdown