jj VCS
jj git init
@
is a special name for “whichever commit the working copy reflects.” Don’t think of it as HEAD, as HEAD is the most recent commit, but @ represents the working copy, which may be “dirty” from git’s point of view. This is indexless workflow.
Log
By default, it omits some commits. Can use revsets.
jj log -r ::
jj log -r 'all()
Workflows
Squash Workflow
Goes something like this:
describe
the work you want to do.- create
new
on top of that. - make changes in that [new] and then use
jj squash
to move them from@
to the one we just described.
This is a bit similar to normal git stuff—you do things, and then do git add
to stage the ones we want in our commit.
- this is because you can squash individual files—
jj squash main.py
.
# Git TUI -- <space> to stage; 'f' to show diff
jj squash -i
Can do jj diffedit
to compare current changes with that of the previous revision (change id).
Edit workflow
- create
new
for a change/feature - make changes
- but you want to add more stuff/break things up into smaller chunks—this is done by: making new -> swap to it -> make the change
- go back to main change
New change
jj new -B @
: create new change before current. Whenever you make a change onto this, the subsequent [existing] changes automatically get rebased as they depend on it.
Back to main change
jj edit <change-id>
But you can do jj next --edit
instead, which moves @
to the child.
- [Note]
jj next
by itself will create a new change after the current one
Branching; Merging; Conflicts
Fetching
jj git fetch
followed by jj rebase -d [main/bookmark]
Branches
Anonymous branches—don’t need to name your branches.
The idea is that, 2 changes that are “branching”, essentially share the same parent.
After a fresh jj new
, do another jj new <parent-commit-id>
jj log 'heads(all())
—shows head of every anonymous branch
Merging
Following a similar concept, merging is basically setting a new change’s parent to the anonymous branches.
jj new <p1> <p2> -m "merged features"
- This creates a merge commit.
Rebasing
Takes a change and moves it to have a different parent, “basing” it again.
# [s]ource; [d]estination
jj rebase -s <c> -d <p>
[use r
instead of s
if you don’t want to rebase the descendents.]
<c>
is the child you want to rebase onto the destination—<p>
(parent). Works well with branches since they share the same parent anyways. Gives us a linear history.
This also lets you change the commits up and down by changing/assigning new parents.
[This feels a bit more modular and a cleaner process than git.]
Conflicts
Naturally you may have changes that are incompatible with each other following a rebase.
Booksmarks & Remotes
They basically act as named branches. You can move it around using set
, and whenever you push the change that’s tagged with that bookmark, it will be pushed onto a branch with the branch’s name being the bookmark’s name.
jj bookmark create [main]
- when you create a bookmark, it’s automatically tracked when you set up an empty remote repo. need a
bookmark
to be able tojj git push
Whenever you abandon a commit that is bookedmarked, the bookmark then moves to the parent.
To make the bookmark point to latest change as it doesn’t automatically go there when you do new
– this makes sense bc depending on your workflow, you make changes in a different way, thus better to wait before updating bookmark:
jj bookmark set [main] --allow-backwards
to swap @
back to prev change.
Remotes
jj git remote add origin <link>
# To update the bookmark to latest change
jj bookmark set [main]
jj git push
Fetching changes
Fetched commits are immutable to ensure you don’t accidently re-write shared commits that might impact others.
Then to start working after the fetched commit—jj new [main]
The local bookmark must not be conflicted, otherwise you have to use jj bookmark move
, for example, to resolve the conflict
Pull Requests
jj git push -c <@/change-id>
: -c
creates new branch (bookmark) from revision.
2 types of workflows:
- some projects require you to address comments by adding commits to your current PR.
- some prefer to just have a 1 whole commit. therefore to address the changes, you’d have to change that revision itself and then force-pushing.
Adding commits to a PR
[Usually done before the maintainer merges the initial req]
# this rev's parent should be the PR branch
jj new -m "respond to feedback"
Then point the PR commit bookmark to point to latest change. Then jj git push
# update bookmark to point to new commit
jj bookmark set <pr-branch-bookmark>
Rebasing a PR (1 change and 1 commit)
Move the bookmark to the initial change if you want to edit something jj bookmark set -r @- --allow-backwards
. Move @
to it as well (jj edit
).
Make your changes and push.
# jj automatically makes it a force push
jj git push --bookmark <bookmark/branch name>
Misc.
jj split
if you want to split up the changes in a revision.
jj undo
- This switches back and forth. If you want to undo operations from a while ago or series of operations, do
jj op log
(shows operations history). One operation is in the form:
○ 5a14e6a03a23 .....
s
│ new empty commit
│ args: jj new l r px
- Everything under
s
is part of that operation id. dojj undo <op-id>
to undo that change.
“??” Bookmarks (conflicted bookmarks)
- set the bookmark to the correct commit
jj bookmark move [main] --to <commit ID>
.
Revsets
Revset is an expression for specifying revisions. Invoked by --revisions
/-r
to select revision(s) to list. Most commands use this under the hood, but omit it. E.g.:
jj edit @+
+
means the child of @
(working copy).
“Use it in a revset” basically means use the [thing] in functions and stuff.
jj abandon <commit>
- delete this copy/revision/commit
gitignore
After you add a path to gitignore, run jj file untrack <path>
.
—Situations
When you edit a few commits before fetching and there’s a bookmark conflict.
- Go to one of the conflicted change-ids
jj git push
(this will make you lose all your merge/pull request commits)- set main back to the head and push