# Pushing changes with jj

How work gets from your working copy to a change on Dogfood.

This repository is a **jj** repo with a git backend. `jj git export` keeps a git view in
sync, which means `git status` and the git reflog see only part of the picture — the
authoritative history is jj's, and `jj op log` is the thing that can undo a mistake.

## The model, in one paragraph

There is no staging area and no "uncommitted work". The working copy **is** a commit
(`@`), and jj snapshots it into that commit before running any command. You do not
create a commit to save work; the work is already in one. What you do instead is give
that commit a description, and then push a bookmark pointing at it. Pushing a bookmark
is what opens or updates a change — there is no web form and no PR button.

## The normal loop

```sh
# 1. Start a new commit on top of the trunk.
jj new main

# 2. Edit files. jj snapshots them into @ automatically — nothing to add.

# 3. Describe what you did.
jj describe -m 'feat: cache shortest-unique prefixes per repo'

# 4. Push. This creates a bookmark named push-<change-id> and opens a change.
jj git push -c @

# 5. Start the next piece of work on top.
jj new
```

Step 4 prints the change URL. That change is now open for review.

## Amending a change after review comments

Because `@` is still the commit you pushed, you just keep editing it and push again:

```sh
# edit files …
jj git push -b push-<change-id>
```

jj reports this as `[move sideways from <old> to <new>]`. "Sideways" means a rewrite
rather than a fast-forward — expected, because you changed a commit that was already
published. No `--force` is required: jj compares the remote against the position it last
recorded, and pushes only when those agree. If someone else moved the bookmark
underneath you, that check fails instead of clobbering their work.

On Dogfood this lands as a **new revision on the same change**. The change id is stable
across every amend, rebase and force-push, which is the whole point — reviews stay
attached to the change, not to a commit hash.

If you have already moved on with `jj new` and `@` is no longer the commit you want to
amend:

```sh
jj edit <change-id>     # make that commit the working copy again
# edit files …
jj git push -b push-<change-id>
jj new                  # go back to working on top
```

## Checking before you push

```sh
jj st                          # what changed in @
jj diff --stat                 # the diff, summarised
jj log -r 'stack(@)'           # the stack this change sits in
jj git push -b <name> --dry-run   # exactly what would move, without moving it
```

`--dry-run` is worth the extra command whenever you are about to rewrite something that
is already published.

## Naming the bookmark yourself

`-c/--change` generates `push-<change-id>`, which is fine for most work. To push under a
name a human chose:

```sh
jj bookmark create my-feature -r @
jj git push -b my-feature
```

## Landing on main

```sh
jj bookmark move main --to @
jj git push -b main
```

This bypasses review entirely, so it is for trivial or already-reviewed work only. The
normal path is the bookmark push above.

## The trap that will cost you a day

```sh
jj new main -m 'feat: my work'    # ← DO NOT use this to save work in progress
```

This does **not** put your current work in the new commit. jj snapshots the working copy
into the **current** commit first, then creates an **empty** commit whose parent is
`main`. You end up pushing an empty change while all your work sits in a now-orphaned
commit labelled `(no description set)` — and because the working copy was reset to
something matching `main`, `git status` reports a clean tree and the work looks deleted.

Use `jj describe -m '…'` to name work you already have. Use bare `jj new` (no revision)
only when you genuinely want to start something new.

### Recovering from it

Nothing is lost — jj keeps every snapshot.

```sh
# 1. Find the stranded commit. It is a sibling with no description.
jj log -r 'all()'

# 2. Confirm it is the right one before touching anything.
jj show --stat <change-id>

# 3. Restore its contents into the current commit.
jj restore --from <change-id>
```

`jj op log` shows the tell: a `snapshot working copy` operation immediately followed by
`new empty commit`, both with the same `args:` line. To rewind the whole repository to
just before a bad command:

```sh
jj op log
jj op restore <operation-id>
```

Prefer `jj restore --from` when you only want the files back and do not want to rewind
bookmarks that were pushed in the meantime.

## Deploying is a separate step

Pushing updates the change. It does not deploy. `./run.sh deploy` builds from the
**working copy on disk**, not from what you pushed — so if the tree is in the stranded
state above, a deploy will quietly ship the previous build. After deploying, confirm
which build is actually live rather than assuming:

```sh
curl -s -o /dev/null -w '%{http_code}\n' https://dogfood.sh/assets/terminal.js
```

Any asset that exists only in the current build works as the probe.

## See also

- [`change-id-format.md`](change-id-format.md) — why change ids look the way they do
- [`revset-semantics.md`](revset-semantics.md) — the revset language used by `-r`
