| 1 | # Pushing changes with jj |
| 2 | |
| 3 | How work gets from your working copy to a change on Dogfood. |
| 4 | |
| 5 | This repository is a **jj** repo with a git backend. `jj git export` keeps a git view in |
| 6 | sync, which means `git status` and the git reflog see only part of the picture — the |
| 7 | authoritative history is jj's, and `jj op log` is the thing that can undo a mistake. |
| 8 | |
| 9 | ## The model, in one paragraph |
| 10 | |
| 11 | There 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 |
| 13 | create a commit to save work; the work is already in one. What you do instead is give |
| 14 | that commit a description, and then push a bookmark pointing at it. Pushing a bookmark |
| 15 | is 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. |
| 21 | jj new main |
| 22 | |
| 23 | # 2. Edit files. jj snapshots them into @ automatically — nothing to add. |
| 24 | |
| 25 | # 3. Describe what you did. |
| 26 | jj 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. |
| 29 | jj git push -c @ |
| 30 | |
| 31 | # 5. Start the next piece of work on top. |
| 32 | jj new |
| 33 | ``` |
| 34 | |
| 35 | Step 4 prints the change URL. That change is now open for review. |
| 36 | |
| 37 | ## Amending a change after review comments |
| 38 | |
| 39 | Because `@` is still the commit you pushed, you just keep editing it and push again: |
| 40 | |
| 41 | ```sh |
| 42 | # edit files … |
| 43 | jj git push -b push-<change-id> |
| 44 | ``` |
| 45 | |
| 46 | jj reports this as `[move sideways from <old> to <new>]`. "Sideways" means a rewrite |
| 47 | rather than a fast-forward — expected, because you changed a commit that was already |
| 48 | published. No `--force` is required: jj compares the remote against the position it last |
| 49 | recorded, and pushes only when those agree. If someone else moved the bookmark |
| 50 | underneath you, that check fails instead of clobbering their work. |
| 51 | |
| 52 | On Dogfood this lands as a **new revision on the same change**. The change id is stable |
| 53 | across every amend, rebase and force-push, which is the whole point — reviews stay |
| 54 | attached to the change, not to a commit hash. |
| 55 | |
| 56 | If you have already moved on with `jj new` and `@` is no longer the commit you want to |
| 57 | amend: |
| 58 | |
| 59 | ```sh |
| 60 | jj edit <change-id> # make that commit the working copy again |
| 61 | # edit files … |
| 62 | jj git push -b push-<change-id> |
| 63 | jj new # go back to working on top |
| 64 | ``` |
| 65 | |
| 66 | ## Checking before you push |
| 67 | |
| 68 | ```sh |
| 69 | jj st # what changed in @ |
| 70 | jj diff --stat # the diff, summarised |
| 71 | jj log -r 'stack(@)' # the stack this change sits in |
| 72 | jj 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 |
| 76 | is 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 |
| 81 | name a human chose: |
| 82 | |
| 83 | ```sh |
| 84 | jj bookmark create my-feature -r @ |
| 85 | jj git push -b my-feature |
| 86 | ``` |
| 87 | |
| 88 | ## Landing on main |
| 89 | |
| 90 | ```sh |
| 91 | jj bookmark move main --to @ |
| 92 | jj git push -b main |
| 93 | ``` |
| 94 | |
| 95 | This bypasses review entirely, so it is for trivial or already-reviewed work only. The |
| 96 | normal path is the bookmark push above. |
| 97 | |
| 98 | ## The trap that will cost you a day |
| 99 | |
| 100 | ```sh |
| 101 | jj new main -m 'feat: my work' # ← DO NOT use this to save work in progress |
| 102 | ``` |
| 103 | |
| 104 | This does **not** put your current work in the new commit. jj snapshots the working copy |
| 105 | into 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 |
| 107 | commit labelled `(no description set)` — and because the working copy was reset to |
| 108 | something matching `main`, `git status` reports a clean tree and the work looks deleted. |
| 109 | |
| 110 | Use `jj describe -m '…'` to name work you already have. Use bare `jj new` (no revision) |
| 111 | only when you genuinely want to start something new. |
| 112 | |
| 113 | ### Recovering from it |
| 114 | |
| 115 | Nothing is lost — jj keeps every snapshot. |
| 116 | |
| 117 | ```sh |
| 118 | # 1. Find the stranded commit. It is a sibling with no description. |
| 119 | jj log -r 'all()' |
| 120 | |
| 121 | # 2. Confirm it is the right one before touching anything. |
| 122 | jj show --stat <change-id> |
| 123 | |
| 124 | # 3. Restore its contents into the current commit. |
| 125 | jj 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 |
| 130 | just before a bad command: |
| 131 | |
| 132 | ```sh |
| 133 | jj op log |
| 134 | jj op restore <operation-id> |
| 135 | ``` |
| 136 | |
| 137 | Prefer `jj restore --from` when you only want the files back and do not want to rewind |
| 138 | bookmarks that were pushed in the meantime. |
| 139 | |
| 140 | ## Deploying is a separate step |
| 141 | |
| 142 | Pushing 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 |
| 144 | state above, a deploy will quietly ship the previous build. After deploying, confirm |
| 145 | which build is actually live rather than assuming: |
| 146 | |
| 147 | ```sh |
| 148 | curl -s -o /dev/null -w '%{http_code}\n' https://dogfood.sh/assets/terminal.js |
| 149 | ``` |
| 150 | |
| 151 | Any 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