Important
This guide is written and checked for Jujutsu 0.45.1
jj is still evolving quickly. If you use a newer release, check its changelog when a command or setting behaves differently
This guide takes you from installing jj to a comfortable daily workflow, then covers everything else for when you need it. Part 1 is all you need for everyday work. Part 2 goes deeper, one topic at a time
It is written for people who are new to jj and know at least the basics of Git. You need Git 2.41 or newer and a terminal: PowerShell on Windows, or Bash or Zsh on Ubuntu. Any text editor works
It covers:
- Windows with PowerShell
- Ubuntu with Bash or Zsh
- Git repositories, so GitHub, your team, and Git tools keep working
The guide comes with an optional set of shell shortcuts. Every command is shown in full, and where a shortcut exists it appears as a comment next to it:
jj status # jsYou never have to use the shortcuts, but they save a lot of typing in the daily flow
Part 1: Daily use
- What
jjis - Install
jj - Configure
jj - Set up your shell
- Open or create a repository
- The everyday loop
- Common tasks
Part 2: In depth
- Look around
- Sync with a remote
- Reshape history
- Resolve conflicts
- Undo and recover
- Bookmarks and tracking
- Tags
- Revsets
- Run commands across revisions
- Workspaces and sparse checkouts
- Sign commits with SSH
- Customize the log
- Maintenance
- Shortcut reference
Appendices
Jujutsu is a version control system that works on top of Git repositories. Your team, GitHub, and your Git tools see an ordinary Git repository, and you get a simpler way of working locally
Five ideas explain most of the difference:
- There is no staging area. The files you are editing are themselves a change, called
@.jjsaves your edits into it automatically every time you run ajjcommand, so there is nogit add - Changes keep their ID. Every change has a change ID, such as
kxmwmlvk, that stays the same when you reword, edit, or move the change. Use change IDs to refer to your work. You only need to type the first few letters, andjj loghighlights how many - Edits ripple forward. When you edit an earlier change, everything built on top of it is updated automatically
- Conflicts don't block you. A conflict is stored inside the change. You can finish what you were doing and resolve it later
- Mistakes can be undone. Every command that changes the repository is recorded, and
jj undoreverses the last one
| Term | Meaning |
|---|---|
@ |
The change you are working in right now |
@- |
The change just below @, its parent |
| Change | One piece of work. It keeps its change ID when rewritten |
| Revision | Anything that names a change: A change ID, a commit ID, a bookmark, or an expression such as @- |
| Bookmark | A name that points at a change, like a Git branch |
trunk() |
Your project's main line, usually main on the remote |
| Stack | Your changes that are not in trunk() yet, from the oldest up to @ |
| Immutable | Protected from rewriting. The main line and tags are immutable, but your own work is not |
| Git | jj |
Shortcut |
|---|---|---|
git status |
jj status |
js |
git diff |
jj diff |
jd |
git log |
jj log |
jl |
git add |
Not needed | |
git commit -m "..." |
jj commit -m "..." |
jcm ... |
git commit --amend |
jj squash moves your new edits into the previous change |
jsq |
git switch -c NAME main |
jj new main, then name it when you publish |
jn main |
git stash |
Not needed: Start other work with jj new main and come back with jj edit |
jn, je |
git restore FILE |
jj restore FILE |
jdiscard FILE |
git pull |
jj git fetch, then jj rebase |
jup |
git push |
jj git push --bookmark NAME |
jpub NAME |
git cherry-pick |
jj duplicate |
jdupe |
git revert |
jj revert |
jrevert |
git reflog |
jj op log and jj undo |
jop, jundo |
Note
This guide uses colocated repositories, which contain both a .jj and a .git directory. Git tools, such as VS Code's Source Control view, keep working alongside jj
jj needs Git 2.41 or newer. Check with:
git --versionIf Git is missing or too old on Windows, install it with winget install Git.Git. Ubuntu 22.04 ships Git 2.34, which is too old. Install a current Git from the Git maintainers' PPA:
sudo add-apt-repository ppa:git-core/ppa
sudo apt update
sudo apt install gitUbuntu 24.04 and newer already ship a recent enough Git
winget install jj-vcs.jjUpgrade later with winget upgrade jj-vcs.jj
There is no official apt package. Download the prebuilt binary into ~/.local/bin:
mkdir -p ~/.local/bin
curl -fsSL https://github.com/jj-vcs/jj/releases/download/v0.45.1/jj-v0.45.1-x86_64-unknown-linux-musl.tar.gz \
| tar -xz -C ~/.local/bin ./jjOn ARM machines, replace x86_64 with aarch64. If jj is not found afterwards, log out and back in so that ~/.local/bin is added to your PATH
To upgrade later, run the same command with the new version number in both places
jj versionThe result should begin with jj 0.45.1
Set your name and email. Every change records them:
jj config set --user user.name "Your Name"
jj config set --user user.email "your-email@example.com"jj opens an editor when you write a description without -m, for example with jj commit or jj describe. Without any setting, it uses your $EDITOR, or Notepad on Windows and nano on Ubuntu. Conflicts open in jj's built-in terminal merge tool. Both work without any setup
To use VS Code instead, if you have it installed:
# Windows
jj config set --user ui.editor "code.cmd -w"
jj config set --user ui.merge-editor "vscode"# Ubuntu
jj config set --user ui.editor "code -w"
jj config set --user ui.merge-editor "vscode"-w makes jj wait until you close the tab. Any other editor works the same way, for example jj config set --user ui.editor vim. For conflicts, jj also knows meld, kdiff3, vimdiff, vscodium, smerge, and mergiraf without extra setup
Interactive commands such as jj split always use jj's built-in terminal diff editor
Open your configuration file in your editor:
jj config edit --userAdd the following:
#:schema https://docs.jj-vcs.dev/latest/config-schema.json
[snapshot]
# Repair a stale working copy automatically instead of asking you to.
auto-update-stale = true
[revset-aliases]
# Your changes that have edits but no description yet.
"wip()" = 'mine() & mutable() & description(exact:"") & ~empty()'
# Empty changes left below where you are working.
"stale()" = "ancestors(@-) & mutable() & empty() & ~merges()"- The
#:schemaline lets a TOML extension such as Even Better TOML validate the file and complete setting names auto-update-stalehandles a rare situation, such as an interrupted command or another workspace rewriting your change, without stopping to ask you to runjj workspace update-stale- The two aliases add
wip()andstale()to the revset language. Thejwipandjstaleshortcuts use them
Useful configuration commands:
jj config path --user # Where the file is
jj config list # Every setting currently in effectCompletion suggests commands, flags, bookmarks, change IDs, and paths when you press Tab
PowerShell. If you have never used a PowerShell profile, create the file first:
if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }Open it with notepad $PROFILE and add:
$env:COMPLETE = "powershell"
try {
jj | Out-String | Invoke-Expression
}
finally {
Remove-Item Env:\COMPLETE -ErrorAction SilentlyContinue
}Save, then reload with . $PROFILE. If PowerShell refuses to load the profile, allow local scripts for your account:
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedZsh. Add to ~/.zshrc, then run source ~/.zshrc:
autoload -Uz compinit
compinit
source <(COMPLETE=zsh jj)Bash. Add to ~/.bashrc, then run source ~/.bashrc:
source <(COMPLETE=bash jj)The shortcut profile adds 125 short commands. You do not need to learn them all: The dozen in the everyday loop cover most days, and the shortcut reference lists the rest
To install them:
- PowerShell: Paste Appendix A: PowerShell profile into
$PROFILEbelow the completion lines, save, and run. $PROFILE - Bash or Zsh: Paste Appendix B: Bash and Zsh profile into
~/.bashrcor~/.zshrc, save, andsourcethe file
Both versions have the same shortcuts and behave the same way. None of them bypass jj's protection of published history, and the few that discard things in bulk (jclean, jopclean, jtagmirror) show what they will do and ask first
Every shortcut starts with j. The rest follows a few patterns:
| Pattern | Meaning | Examples |
|---|---|---|
j + the start of a command |
That jj command |
js status, jl log, jd diff, jn new, je edit, ja abandon, jsq squash, jrb rebase |
Ends in m |
Takes a message, no quotes needed | jm describe, jnm new, jcm commit |
jb… |
Bookmarks | jbc create, jbm move, jbd delete, jbl list |
jt… |
Tags | jtc create, jtm move, jtd delete, jtl list |
jg… |
Git remotes | jgf fetch, jgp push, jgra add a remote |
jop… |
The operation log | jop log, jopshow, joprevert, joprestore |
jf… |
Files in a revision | jfshow, jflist, jfa annotate |
Where a short name would be unclear, the word is spelled out: jrestore, jrevert, jdiscard, jdupe, jsplit, jundo, jpub (publish), jup (update)
Many jj commands treat a bare argument as a file path. For example, jj restore abc restores a file named abc. The shortcuts fill in the flag you almost always mean instead:
- Plain values fill the shortcut's slots in order.
jrestore abcrunsjj restore --from abc, andjbm feat abcmoves bookmarkfeattoabc - Anything after the slots goes to
jjunchanged, whether flags or file paths:jrestore abc src/app.ts --interactive - If the first argument is a flag, no slots are filled. You are writing normal
jjarguments:jrestore --changes-in abc - Defaults apply only when you have not said otherwise.
jrevert abcplaces the revert below@, butjrevert abc --onto mainplaces it onmain
When you leave out a revision, it defaults to @. The publishing shortcuts jpub, jpc, and jtsp are smarter: If @ is empty and has no description, as it is right after jcm, they use @- instead and tell you so
Shortcuts that talk to a remote use the repository's only remote. If there are several, pass the remote name as the last argument
Important
In PowerShell, quote every revision that starts with @, such as '@-'. PowerShell treats an unquoted @ as its own syntax and stops with Unrecognized token in source text before jj runs. Single quotes work in every shell, so this guide uses them throughout
Clone an existing project:
jj git clone https://github.com/owner/project.git # jclone URL
cd projectStart using jj in a Git repository you already have, from its root folder:
jj git init --colocate # jiCreate a brand-new repository:
mkdir my-project
cd my-project
jj git initThen take a look:
jj status # js
jj log # jlIf jj log does not show your main bookmark, see Bookmarks and tracking
This is the workflow for most days. Each step shows the shortcut and the full command
jj new main # jn mainThere is no add step. Edit your files, then check them:
jj status # js
jj diff # jdjj commit -m "Add the login form" # jcm Add the login formjj commit gives your change a description and starts a new, empty change on top, ready for the next piece of work. Repeat steps 2 and 3 as often as you like
Your history now looks like this in jj log:
@ qktosxrt you@example.com 2026-09-25 23:31:58 70dee824
│ (empty) (no description set)
○ nonwzlvy you@example.com 2026-09-25 23:31:58 d343dcb0
│ Validate login input
○ zomwqpvz you@example.com 2026-09-25 23:31:58 1ba6307f
│ Add the login form
◆ vyzvlssm you@example.com 2026-09-25 23:31:57 main bc650d6a
│ Update the README
~
@ is the fresh empty change you are in. @- is the change you just finished. The diamond ◆ marks immutable history
jpub feature/loginjpub points the bookmark feature/login at your latest finished change and pushes it. Run the same command again whenever you have finished more work. Without the shortcut:
jj bookmark set feature/login -r '@-'
jj git push --bookmark feature/loginjupjup fetches from the remote and moves your work on top of the latest main line:
jj git fetch
jj rebase --onto 'trunk()'If you had already published, run jpub feature/login again to update the remote
jj undo # jundoUndo reverses local operations such as rebases, squashes, abandons, and bookmark moves. Run it again to go further back. It cannot take back a push that has already reached the remote
- Fix the last change instead of starting a new one: Make the edits, then run
jsq(jj squash). It moves them into@-, likegit commit --amend - Reword a description:
jm New wordingrewords@. For the change you just finished, usejj describe '@-' -m "New wording" - Describe first, work second:
jnm Add the login formstarts a change that already has its description. Edit, then runjnmagain for the next change - Edit an older change directly:
je CHANGE_IDmakes it@, and the changes above it update automatically as you edit. When you are done, go back withjeand the ID of your newest change
Find what you want to do, then use the shortcut or the full command. abc and def stand for change IDs, and NAME for a bookmark
| I want to | Shortcut | Full command |
|---|---|---|
| See what I changed | jd |
jj diff |
| See only the changed file names | jfiles |
jj diff --summary |
| See my stack | jstack |
jj log -r 'trunk()..@' |
| See status and my stack together | jnow |
jj status, then the above |
| See one change in full | jsh abc |
jj show abc |
| Compare two changes | jd abc def |
jj diff --from abc --to def |
| See how a change evolved | jevo abc |
jj evolog -r abc |
| See the whole history, not just recent work | jla |
jj log -r 'all()' |
| I want to | Shortcut | Full command |
|---|---|---|
| Start new work on main | jn main |
jj new main |
| Finish this change and start the next | jcm Message |
jj commit -m "Message" |
| Reword the current change | jm Message |
jj describe -m "Message" |
| Go back and edit an earlier change | je abc |
jj edit abc |
| Add my new edits to the previous change | jsq |
jj squash |
| Add my new edits to a specific change | jsqi abc |
jj squash --into abc |
| Choose which lines to move to the previous change | jsqp |
jj squash --interactive |
| Send each fix to the earlier change it belongs to | jabs |
jj absorb |
| Split the current change in two | jsplit |
jj split |
| Throw away my current edits | jdiscard [PATHS] |
jj restore [PATHS] |
| Delete a change | ja abc |
jj abandon abc |
| I want to | Shortcut | Full command |
|---|---|---|
| Copy a change and continue on the copy | jdupe abc |
jj duplicate abc, then jj edit the new ID |
| Copy a change on top of another one | jdupe abc --onto main |
jj duplicate abc --onto main, then jj edit |
| Bring files back from another change | jrestore abc [PATHS] |
jj restore --from abc [PATHS] |
| Add a change that undoes an earlier one | jrevert abc |
jj revert -r abc --insert-before '@' |
| I want to | Shortcut | Full command |
|---|---|---|
| Move my stack onto the latest main | jup |
jj git fetch, then jj rebase --onto 'trunk()' |
| Move my stack somewhere else | jrb main |
jj rebase --onto main |
| Move a change and everything above it | jrbs abc main |
jj rebase --source abc --onto main |
| Move only one change | jrbr abc main |
jj rebase --revisions abc --onto main |
| I want to | Shortcut | Full command |
|---|---|---|
| Publish my work under a name | jpub NAME |
jj bookmark set NAME -r '@-', then jj git push --bookmark NAME |
| Publish without choosing a name | jpc |
jj git push --change '@-' |
| Preview a push | jgpp NAME |
jj git push --bookmark NAME --dry-run |
| Move a bookmark anywhere | jbm NAME abc |
jj bookmark move NAME --to abc --allow-backwards |
| Delete a bookmark here and on the remote | jbdp NAME |
jj bookmark delete NAME, then jj git push --bookmark NAME |
| Tag a release and push the tag | jtsp v1.2.0 |
jj tag set v1.2.0 -r '@-', then jj git push --tag v1.2.0 |
| I want to | Shortcut | Full command |
|---|---|---|
| Undo the last command | jundo |
jj undo |
| Redo what I undid | jredo |
jj redo |
| See every command I have run | jop |
jj op log |
jj status # js
jj log # jl
jj show # jsh
jj diff # jd
jj diff --stat # jds
jj diff --summary # jfilesVariations:
# Log a revset
jj log -r 'main..@' # jl 'main..@'
# Show one change, or several oldest-first
jj show abc # jsh abc
jj show --reversed -r 'trunk()..@' # jreview
# Diff one change, or compare two
jj diff -r abc # jd abc
jj diff --from main --to '@' # jd main '@'
# Compare what two versions of a change did
jj interdiff --from abc --to def # jidiff abc def
# See every earlier version of a change
jj evolog -r abc # jevo abcjstack and jreview also accept a bookmark or change: jstack feature/login shows the changes on that bookmark that are not yet in trunk()
Start a new change on top of any revision:
jj new abc # jn abcEdit an existing change directly:
jj edit abc # je abc
jj edit '@-' # jepStep through a stack:
jj prev # jprev
jj next # jnextBy default, prev and next create a new empty change at the destination. Add --edit to edit the destination directly instead
jj has no pull command. Fetching and rebasing are separate steps, and jup combines them
jj git fetch # jgf
jj git fetch --remote upstream # jgf upstream
jj git fetch --all-remotes # jgfajj rebase --onto 'trunk()' # jrb 'trunk()'Without --source or --revisions, jj rebase moves your whole stack: Every change between trunk() and @. Descendants always come along
jup fetches and then runs exactly this rebase. Pass a remote name to fetch only that remote: jup upstream
Note
Some people configure jj to treat everything they have pushed as immutable (see Immutable history). With that policy, jup stops with Commit ... is immutable once your stack has been published. That is the policy doing its job. If you really mean to rewrite the published changes, run jj git fetch and then jj --ignore-immutable rebase --onto 'trunk()'
jpub, from the everyday loop, names your work and pushes it in one step:
jpub feature/login # Name your latest finished change and push it
jpub feature/login abc # Name and push a specific change instead
jpc # Push with an automatically generated bookmark nameWhen the bookmark already points where you want, push it directly:
jj git push --bookmark feature/login # jgp feature/login
jj git push --bookmark feature/login --dry-run # jgpp feature/loginA plain jj git push (jgp) pushes every tracked bookmark and tag that points between the remote's bookmarks and @
Pushing is safe by default. When a bookmark was rewritten, for example after jup, jj updates the remote only if the remote still matches what you last fetched, like Git's --force-with-lease. You do not need a separate force option for normal work
For the rare case where you must overwrite the remote regardless of what is there:
jgpforce feature/loginjgpforce force-pushes only the named bookmark through Git, then fetches the result
Caution
An unconditional force-push can overwrite work that others pushed since your last fetch. Use it only when that is what you intend
Push every bookmark and tag at once:
jj git push --all # jgpaWarning
--all is broad and, since jj 0.44, also pushes every tag. Prefer pushing one named bookmark
jbdp feature/loginWithout the shortcut:
jj bookmark delete feature/login
jj git push --bookmark feature/loginDeleting a bookmark does not delete its changes. They remain recoverable through the operation log
jj is built for editing local history. A change keeps its change ID when you rewrite it, but gets a new commit ID
# Move all edits in @ into its parent
jj squash # jsq
# Squash a specific change into its parent
jj squash -r abc # jsq abc
# Choose which lines to move
jj squash --interactive # jsqp
# Move edits from @ into any mutable change
jj squash --into abc # jsqi abc
# Only certain files
jj squash --into abc src/app.ts # jsqi abc src/app.tsjj split # jsplit
jj split -r abc # jsplit abcThe built-in editor lets you choose what belongs in the first change, and the rest becomes a second change on top
jj absorb # jabs
jj absorb --interactive # jabs --interactiveabsorb moves each edit in @ into the earlier change that last touched those lines. Use it to apply review fixes across a stack
# Move your stack
jj rebase --onto main # jrb main
# Move a change and everything above it
jj rebase --source abc --onto main # jrbs abc main
# Move only certain changes. Their children stay in place
jj rebase --revisions abc --onto main # jrbr abc mainjj duplicate abc # Copy it and stay where you are
jj duplicate abc --onto main # Copy it somewhere elsejdupe duplicates and then switches to the copy so that you can edit it straight away:
jdupe # Copy @
jdupe abc
jdupe abc --onto mainIf you copy several changes at once, jdupe stays where you are and prints the new IDs
| Command | Shortcut | Use it to |
|---|---|---|
jj arrange |
jarr |
Reorder and move changes interactively in one screen |
jj parallelize REVSET |
jpar |
Turn a line of changes into siblings |
jj diffedit -r abc |
jdiffedit abc |
Edit a change's content in the diff editor |
jj fix -s abc |
jfix abc |
Run configured formatters over changes |
jj simplify-parents -r abc |
jsimp abc |
Remove redundant parent edges |
jj metaedit |
jme |
Change a change's author, timestamps, or change ID without touching its content |
A change is divergent when two visible commits share its change ID, for example after it was rewritten in two places. Find and combine them:
jj log -r 'divergent()' # jdiv
jj converge # jconvjj converge asks for help when it cannot decide how to combine them
A conflict is stored in the change, so a rebase can finish and leave the conflict for you to resolve when convenient
jj log -r 'conflicts()' # jconf Which changes have conflicts
jj resolve --list # jresl Which files in @ are conflicted
jj resolve # jres Open each one in your merge tool
jj resolve path/to/file # jres path/to/filejj resolve uses jj's built-in terminal tool, or the merge tool you chose in Configure jj. Save the result and close the tool
You can also fix a conflicted file by hand: Remove the conflict markers, keep the right content, and save. jj notices on the next command
To resolve a conflict in an earlier change, edit it first with je abc, or start a change on top of it with jn abc, resolve, and then jsq
Every command that changes the repository is recorded in the operation log
jj undo # jundo Undo the last operation. Repeat to go further back
jj redo # jredo
jj op log # jop List operations
jj op show OP # jopshow What one operation changed
jj op diff --from OP_A --to OP_B # jopdiff| Command | Shortcut | Use it when |
|---|---|---|
jj undo |
jundo |
The last thing you did was a mistake |
jj op revert OP |
joprevert OP |
One particular earlier operation should be reversed, keeping everything since |
jj op restore OP |
joprestore OP |
The whole repository should go back to how it was at that operation |
jj revert -r abc --insert-before '@' |
jrevert abc |
You want a new change that undoes the code of an earlier change |
Caution
op restore also undoes everything after that operation. Look at the state first:
jj --at-op=OP logA bookmark is a name that points at a change. Unlike a Git branch, it does not move when you make new changes. You move it yourself, or jpub moves it for you
| Goal | Command | Shortcut |
|---|---|---|
| List | jj bookmark list --all-remotes |
jbl --all-remotes |
| Create at a change | jj bookmark create NAME -r abc |
jbc NAME abc |
| Create, or move forward | jj bookmark set NAME -r abc |
jbs NAME abc |
| Move in any direction | jj bookmark move NAME --to abc --allow-backwards |
jbm NAME abc |
Move the nearest bookmark up to @ |
jj bookmark advance |
jba |
| Rename | jj bookmark rename OLD NEW |
jbr OLD NEW |
| Delete | jj bookmark delete NAME |
jbd NAME |
| Delete here and on the remote | jbdp NAME |
If the revision is left out, these use @. jbm refuses to create a bookmark that does not exist, so a typo cannot create a new one
Tracking links a local bookmark to the remote bookmark with the same name, so fetches update it and jj git push knows where it belongs. Pushing a new bookmark tracks it automatically
jj bookmark track main@origin # jbt main@origin
jj bookmark untrack main@origin # jbu main@originWhen you add jj to an existing Git repository, check that your main line is tracked:
jj bookmark list --all-remotes
jj bookmark track main@originYou can automate tracking per remote. Pick the policy that fits the repository, and do not copy one blindly into a shared configuration:
# Track every bookmark on this remote, suitable for a personal fork
[remotes.origin]
auto-track-bookmarks = "*"# Track only bookmarks you create
[remotes.origin]
auto-track-created-bookmarks = "*"# Track only your own namespace
[remotes.origin]
auto-track-bookmarks = "yourname/*"Since jj 0.44, tags work much like bookmarks: jj git fetch fetches remote tags as NAME@REMOTE and tracks them, and jj git push --tag pushes them
| Goal | Command | Shortcut |
|---|---|---|
| List | jj tag list |
jtl |
| List with remote state | jj tag list --all-remotes |
jtla |
| Create | jj tag set v1.2.0 -r abc |
jtc v1.2.0 abc |
| Move | jj tag set v1.2.0 -r abc --allow-move |
jtm v1.2.0 abc |
| Delete | jj tag delete v1.2.0 |
jtd v1.2.0 |
| Push one tag | jj git push --tag v1.2.0 |
jtp v1.2.0 |
| Create and push | jtsp v1.2.0 [abc] |
|
| Delete here and on the remote | jtdp v1.2.0 |
|
| List the remote's tags without fetching | git ls-remote --tags --refs origin |
jtrl |
jj tag set refuses to move an existing tag unless you add --allow-move, so jtc cannot move a tag by accident
A tag you create locally is not tracked until you push it with --tag or run jj tag track NAME@REMOTE. Once tracked, a plain jj git push also pushes it when it is in the default push range. If a tracked tag is deleted on the remote, the next fetch deletes it locally too. To stop fetching tags from a remote, set remotes.<name>.fetch-tags = "~*"
To push every pending tag change at once:
jj git push --tag '*' --dry-run # Preview
jtagmirror # Preview, then push after you type yesjtagmirror pushes every new, moved, and deleted tag. Tags that exist only on the remote are not deleted. jj git fetch brings them in instead
A revset is an expression that selects changes. Anything that takes a revision accepts one, for example jj log -r REVSET or jl REVSET
jj log -r '@' # The current change
jj log -r '@- | @--' # Parent and grandparent
jj log -r 'trunk()..@' # Your changes not yet in the main line
jj log -r 'main::' # main and everything built on it
jj log -r 'mine() & mutable()' # Your rewritable changes (jmine)
jj log -r 'conflicts()' # Changes with conflicts (jconf)
jj log -r 'empty()' # Empty changes
jj log -r 'files("src/main.rs")' # Changes touching a path
jj log -r 'description(substring:"login")' # Descriptions containing "login"
jj log -r 'remote_bookmarks()' # Every remote bookmark
jj log -r 'forks()' # Changes with more than one childx..y means "reachable from y but not from x". x::y means "descendants of x that are ancestors of y"
The wip() and stale() aliases from Configure jj work anywhere a revset does. jprivate logs the git.private-commits revset, which lists changes jj must never push. It is empty until you define it
jj refuses to rewrite immutable history. By default, that is:
trunk(), tags, and untracked remote bookmarks and tags- All of their ancestors
- The root commit
jj log -r 'immutable()'To rewrite something immutable anyway, say so explicitly:
jj --ignore-immutable describe abc -m "Corrected message"Warning
Do not turn --ignore-immutable into a shortcut. Rewriting published history should stay a visible, deliberate choice. The safer alternative is a new change on top: jj new main -m "Fix the published change"
Some people also protect everything they have pushed by adding this to their configuration:
[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads() | remote_bookmarks(remote=origin)"With it, a pushed change can no longer be squashed into, split, or rebased by jup. It is a valid choice, but it makes the everyday loop stricter: Once published, new work goes in new changes on top
jj run checks out each selected revision in a separate working copy, runs a command in it, and keeps any files the command changes. It goes from oldest to newest and stops at the first failure
jj run -r '@' -- npm test # jrun npm test
jj run -r 'trunk()..@' -- npm test # jrunstack npm test
jj run -r 'trunk()..@' -j 4 -- npm run lint # jrunstack -j 4 npm run lintWithout -r, jj run selects every mutable revision connected to @, not only the current one. jrun therefore defaults to -r '@'. It also adds the -- separator, which PowerShell would otherwise remove
| Option | Effect |
|---|---|
--ignore-changes |
Run without rewriting anything, which suits tests |
--ignore-errors |
Continue after a failure |
--passthrough |
Show the command's output live. Works with one job only |
Put options before the command: jrun --ignore-changes npm test
Caution
A command that changes files rewrites every selected revision. Check the selection first with jj log -r REVSET
A workspace is another folder attached to the same repository, with its own @. Use one to review or test something without disturbing your current work
jj workspace add ../project-review # jws add ../project-review
jj workspace list # jws list
jj workspace forget NAME # jws forget NAMEIf a workspace reports that it is stale, run jj workspace update-stale. The auto-update-stale setting from Configure jj normally does this for you
Sparse patterns limit which paths appear in the working copy, which helps in very large repositories
jj sparse set --clear --add README.md --add src # Keep only these
jj sparse set --add docs # Add a path
jj sparse set --remove docs # Remove a path
jj sparse list # Show the patterns
jj sparse reset # Bring back everything
jj sparse edit # Edit the patterns in your editorjsparse is a shortcut for jj sparse
Signing is optional. Set it up when your hosting provider or project requires verified commits
ssh-keygen -t ed25519 -C "your-email@example.com"Protect it with a passphrase. The private key is ~/.ssh/id_ed25519 and must never be shared. The public key is ~/.ssh/id_ed25519.pub
Add it to your SSH agent so that you type the passphrase once per session instead of on every signature
On Windows, the agent service is turned off by default. Turn it on once from a PowerShell window opened as administrator:
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agentThen add the key from a normal PowerShell window:
ssh-add "$env:USERPROFILE\.ssh\id_ed25519"On Ubuntu:
ssh-add ~/.ssh/id_ed25519If ssh-add says it cannot connect to the agent, start one first with eval "$(ssh-agent -s)"
Add to jj config edit --user:
[signing]
behavior = "own"
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"
[ui]
show-cryptographic-signatures = truebehavior = "own"signs every change you author wheneverjjcreates or rewrites itshow-cryptographic-signaturesshows each commit's signature status in the log
Create an allowed_signers file that lists trusted keys
PowerShell:
$Email = "your-email@example.com"
$PublicKey = (Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" -Raw).Trim()
[IO.File]::WriteAllText(
"$env:USERPROFILE\.ssh\allowed_signers",
"$Email $PublicKey`n",
[Text.UTF8Encoding]::new($false)
)Bash or Zsh:
printf '%s %s\n' "your-email@example.com" "$(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signersPoint jj at it:
[signing.backends.ssh]
# Windows:
allowed-signers = "C:\\Users\\YourName\\.ssh\\allowed_signers"
# Ubuntu:
# allowed-signers = "~/.ssh/allowed_signers"Sign or unsign existing changes with jj sign -r abc (jsign abc) and jj unsign -r abc (junsign abc)
If you also commit with Git in the same repositories, configure Git separately in ~/.gitconfig:
[user]
name = Your Name
email = your-email@example.com
signingkey = ~/.ssh/id_ed25519.pub
[commit]
gpgsign = true
[gpg]
format = sshOn Windows, a forward-slash path such as C:/Users/YourName/.ssh/id_ed25519.pub avoids backslash escaping
The built-in templates are the fastest and most stable:
[templates]
log = "builtin_log_compact" # The default
# log = "builtin_log_compact_full_description" # Show whole descriptionsThis template shows IDs, bookmarks, and signature status on the first line, the description on the second, and the author and time on the third:
[template-aliases]
'format_sig(sig)' = '''
if(
!sig,
"○",
if(sig.status() == "good", "✓", "!")
)
'''
[templates]
log = '''
if(
root,
format_root_commit(self),
label(
separate(
" ",
if(current_working_copy, "working_copy"),
if(immutable, "immutable", "mutable"),
if(conflict, "conflicted"),
),
concat(
separate(
" ",
format_short_change_id(change_id),
format_short_commit_id(commit_id),
format_sig(self.signature()),
bookmarks,
tags,
working_copies,
) ++ "\n",
separate(
" ",
if(empty, label("empty", "(empty)")),
if(conflict, label("conflict", "conflict")),
if(description, description.first_line(), description_placeholder),
) ++ "\n",
" " ++ format_timestamp(commit_timestamp(self))
++ " " ++ format_short_signature(author)
++ "\n",
),
)
)
'''Checking signatures can slow down large logs, because jj may call the signing tool for each commit. Switch back to the built-in template if that happens
Day-to-day work needs no cleanup. Old operations are kept on purpose so that jj undo and jj op restore keep working
jclean # Show the changes, then ask
jclean -y # Show the changes and abandon them without askingjclean selects only changes that are:
- Mutable
- Authored by you
- Not below any bookmark, tag, remote bookmark, remote tag, or workspace
It shows them before abandoning. jj undo brings them back. Bash and Zsh also accept --yes
jj config gc # jcfgcRemoves stored per-repository settings for repositories that no longer exist
Find the newest operation you want to discard in jop, then:
jopclean OPERATION_IDIt shows that operation and asks you to type abandon. It then runs jj op abandon ..OPERATION_ID, which discards that operation and everything older
Warning
Discarded operations can no longer be undone or restored, and the old commits they protected can be removed by garbage collection
jj util gc # jgc
jj util gc --expire now # jgc --expire nowRemoves objects nothing refers to anymore. Normally only objects older than two weeks are removed. With --expire now, all of them are removed, including any made unreachable by jopclean
Every shortcut in the profile. [X] is optional. A revision you leave out means @
| Shortcut | Runs |
|---|---|
j ARGS |
jj ARGS |
jb, jt, jg, jws, jsparse, jcfg, jutil |
jj bookmark, jj tag, jj git, jj workspace, jj sparse, jj config, jj util, followed by your arguments |
ji |
jj git init --colocate |
jclone URL |
jj git clone URL |
| Shortcut | Runs |
|---|---|
js |
jj status |
jl [REVSET] |
jj log -r REVSET |
jla |
jj log -r 'all()' |
jll [N] |
jj log --limit N, 20 by default |
jlb [REV] [N] |
jj log -r 'ancestors(REV)' --limit N, with defaults trunk() and 20 |
jlbroot [REV] |
jj log -r 'root()::REV' --reversed, with trunk() by default |
jstack [REV] |
jj log -r 'trunk()..REV' |
jreview [REV] |
jj show --reversed -r 'trunk()..REV' |
jnow |
jj status, then jstack |
jmine |
jj log -r 'mine() & mutable()' |
jconf |
jj log -r 'conflicts()' |
jwip, jstale |
jj log -r 'wip()', jj log -r 'stale()' |
jdiv |
jj log -r 'divergent()' |
jprivate |
jj log of the git.private-commits revset |
jsh [REV] |
jj show REV |
jd [REV], jd A B |
jj diff -r REV, jj diff --from A --to B |
jds, jfiles |
Like jd, adding --stat or --summary |
jevo [REV] |
jj evolog -r REV |
jidiff A B |
jj interdiff --from A --to B |
| Shortcut | Runs |
|---|---|
jn [REV] |
jj new REV |
jnm MESSAGE |
jj new -m "MESSAGE" |
jm MESSAGE |
jj describe -m "MESSAGE" |
jcm MESSAGE |
jj commit -m "MESSAGE" |
jci, jdesc |
jj commit, jj describe, writing the message in your editor |
jme |
jj metaedit |
je REV |
jj edit REV |
jep |
jj edit '@-' |
jnext, jprev |
jj next, jj prev |
jrestore REV [PATHS] |
jj restore --from REV [PATHS] |
jdiscard [PATHS] |
jj restore [PATHS], discarding edits in @ |
jrevert REV |
jj revert -r REV --insert-before '@' |
ja [REV] |
jj abandon REV |
| Shortcut | Runs |
|---|---|
jsq [REV] |
jj squash -r REV |
jsqi TARGET [PATHS] |
jj squash --into TARGET [PATHS] |
jsqp [REV] |
jj squash --interactive -r REV |
jsplit [REV] |
jj split -r REV |
jabs [REV] |
jj absorb --from REV |
jfix [REV] |
jj fix -s REV |
jdupe [REV] |
jj duplicate REV, then jj edit the copy |
jrb DEST |
jj rebase --onto DEST |
jrbs SOURCE DEST |
jj rebase --source SOURCE --onto DEST |
jrbr REVS DEST |
jj rebase --revisions REVS --onto DEST |
jarr |
jj arrange |
jpar REVSET |
jj parallelize REVSET |
jsimp [REV] |
jj simplify-parents -r REV |
jdiffedit [REV] |
jj diffedit -r REV |
jconv |
jj converge |
jrun COMMAND |
jj run -r '@' -- COMMAND |
jrunstack COMMAND |
jj run -r 'trunk()..@' -- COMMAND |
jbis |
jj bisect |
| Shortcut | Runs |
|---|---|
jres [PATHS] |
jj resolve [PATHS] |
jresl |
jj resolve --list |
| Shortcut | Runs |
|---|---|
jbl |
jj bookmark list |
jbc NAME [REV] |
jj bookmark create NAME -r REV |
jbs NAME [REV] |
jj bookmark set NAME -r REV |
jbm NAME [REV] |
jj bookmark move NAME --to REV --allow-backwards |
jba |
jj bookmark advance |
jbd NAME |
jj bookmark delete NAME |
jbdp NAME [REMOTE] |
Delete locally, then push the deletion |
jbr OLD NEW |
jj bookmark rename OLD NEW |
jbt NAME@REMOTE, jbu NAME@REMOTE |
jj bookmark track, jj bookmark untrack |
| Shortcut | Runs |
|---|---|
jgf [REMOTE] |
jj git fetch --remote REMOTE |
jgfa |
jj git fetch --all-remotes |
jup [REMOTE] |
Fetch, then jj rebase --onto 'trunk()' |
jgp [NAME] |
jj git push --bookmark NAME, or jj git push without a name |
jgpp NAME [REMOTE] |
jj git push --bookmark NAME --dry-run |
jpub NAME [REV] [REMOTE] |
jj bookmark set NAME -r REV, then push it |
jpc [REV] [REMOTE] |
jj git push --change REV |
jgpa |
jj git push --all: Every bookmark and tag |
jgpforce NAME [REMOTE] |
Unconditional force-push of one bookmark |
jgra, jgrr, jgrl, jgrn, jgru |
jj git remote add, remove, list, rename, set-url |
jgroot |
jj git root |
| Shortcut | Runs |
|---|---|
jtl, jtla |
jj tag list, adding --all-remotes for jtla |
jtc NAME [REV] |
jj tag set NAME -r REV |
jtm NAME [REV] |
jj tag set NAME -r REV --allow-move |
jtd NAME |
jj tag delete NAME |
jtp NAME [REMOTE] |
jj git push --tag NAME |
jtsp NAME [REV] [REMOTE] |
Create the tag, then push it |
jtdp NAME [REMOTE] |
Delete the tag, then push the deletion |
jtrl [REMOTE] |
List the remote's tags without fetching |
jtagmirror [REMOTE] |
Preview every tag change, then push after you type yes |
| Shortcut | Runs |
|---|---|
jundo, jredo |
jj undo, jj redo |
jop |
jj op log |
jopshow OP, jopdiff |
jj op show OP, jj op diff |
joprevert OP |
jj op revert OP |
joprestore OP |
jj op restore OP |
jopclean OP |
jj op abandon ..OP after you type abandon |
| Shortcut | Runs |
|---|---|
jfshow PATH |
jj file show PATH |
jflist [REV] |
jj file list -r REV |
jfa PATH |
jj file annotate PATH |
jftrack, jfuntrack |
jj file track, jj file untrack |
jfchmod x PATH |
jj file chmod x PATH. Use n instead of x to remove the executable bit |
| Shortcut | Runs |
|---|---|
jsign [REV], junsign [REV] |
jj sign -r REV, jj unsign -r REV |
jcfgc |
jj config gc |
jgc |
jj util gc |
jclean [-y] |
Abandon your disconnected changes after showing them |
jroot, jcd, jcode |
Print the repository root, go to it, or open it in VS Code if you have it |
jver, jh |
jj version, jj help |
Paste this into $PROFILE below the completion lines from Set up your shell
# Pass-through and repository setup
function j { jj @args }
function ji { jj git init --colocate @args }
function jclone { jj git clone @args }
# Argument handling shared by the shortcuts: leading plain values fill a shortcut's
# slots, flags go to jj unchanged, and defaults apply only when you did not type the
# matching flag yourself.
function _jjIsFlag { param($Value) "$Value".StartsWith('-') }
function _jjHasFlag {
param([object[]]$Arguments, [string[]]$Flags)
foreach ($Argument in $Arguments) {
if ($Flags -ccontains ("$Argument" -replace '=.*$', '')) { return $true }
}
$false
}
function _jjRev {
# jj COMMAND, where a leading plain value becomes "FLAG value".
param([string[]]$Command, [string]$Flag, [object[]]$Arguments)
$JJArgs = @($Command)
$Rest = @($Arguments)
if ($Rest.Count -gt 0 -and -not (_jjIsFlag $Rest[0])) {
$JJArgs += $Flag, $Rest[0]
$Rest = @($Rest | Select-Object -Skip 1)
}
jj @JJArgs @Rest
}
function _jjRange {
# Like _jjRev, but two leading plain values become "--from A --to B".
param([string[]]$Command, [string]$Flag, [object[]]$Arguments)
$Rest = @($Arguments)
if ($Rest.Count -ge 2 -and -not (_jjIsFlag $Rest[0]) -and -not (_jjIsFlag $Rest[1])) {
$From, $To = $Rest[0], $Rest[1]
$Rest = @($Rest | Select-Object -Skip 2)
jj @Command --from $From --to $To @Rest
return
}
_jjRev $Command $Flag $Rest
}
function _jjNamed {
# jj COMMAND NAME, where a second plain value becomes "FLAG value".
param([string[]]$Command, [string]$Flag, [object[]]$Arguments)
$JJArgs = @($Command)
$Rest = @($Arguments)
if ($Rest.Count -gt 0 -and -not (_jjIsFlag $Rest[0])) {
$JJArgs += $Rest[0]
$Rest = @($Rest | Select-Object -Skip 1)
if ($Rest.Count -gt 0 -and -not (_jjIsFlag $Rest[0])) {
$JJArgs += $Flag, $Rest[0]
$Rest = @($Rest | Select-Object -Skip 1)
}
}
jj @JJArgs @Rest
}
function _jjStack {
# jj COMMAND for trunk()..REVISION, where a leading plain value replaces @.
param([string[]]$Command, [object[]]$Arguments)
$Rest = @($Arguments)
$Revision = '@'
if ($Rest.Count -gt 0 -and -not (_jjIsFlag $Rest[0])) {
$Revision = $Rest[0]
$Rest = @($Rest | Select-Object -Skip 1)
}
jj @Command -r "trunk()..($Revision)" @Rest
}
function _jjRebaseOnto {
# jj rebase FLAG FIRST --onto SECOND. A leading flag passes everything through.
param([string]$Flag, [string]$Usage, [object[]]$Arguments)
$Rest = @($Arguments)
if ($Rest.Count -gt 0 -and (_jjIsFlag $Rest[0])) {
jj rebase @Rest
return
}
if ($Rest.Count -lt 2 -or (_jjIsFlag $Rest[1])) {
Write-Error $Usage
return
}
$Tail = @($Rest | Select-Object -Skip 2)
jj rebase $Flag $Rest[0] --onto $Rest[1] @Tail
}
# Status, logs, and diffs
function js { jj status @args }
function jl { _jjRev log '-r' $args }
function jla { jj log -r "all()" @args }
function jll {
param([int]$Limit = 20)
jj log --limit $Limit @args
}
function jlb {
param($Bookmark = "trunk()", [int]$Limit = 20)
jj log -r "ancestors($Bookmark)" --limit $Limit @args
}
function jlbroot {
param($Bookmark = "trunk()")
jj log -r "root()::$Bookmark" --reversed @args
}
function jstack { _jjStack log $args }
function jreview { _jjStack @('show', '--reversed') $args }
function jnow {
jj status
if ($LASTEXITCODE -eq 0) {
jstack
}
}
function jmine { jj log -r "mine() & mutable()" @args }
function jconf { jj log -r "conflicts()" @args }
function jwip { jj log -r "wip()" @args }
function jstale { jj log -r "stale()" @args }
function jdiv { jj log -r "divergent()" @args }
function jprivate {
$PrivateRevset = jj config get git.private-commits
if ($LASTEXITCODE -eq 0) {
jj log -r $PrivateRevset @args
}
}
function jsh { jj show @args }
function jd { _jjRange diff '-r' $args }
function jds { _jjRange @('diff', '--stat') '-r' $args }
function jfiles { _jjRange @('diff', '--summary') '-r' $args }
function jevo { _jjRev evolog '-r' $args }
function jidiff { _jjRange interdiff '--from' $args }
# Create, edit, and restore changes
function jn { jj new @args }
function jnm {
if ($args.Count -eq 0) {
Write-Error "Usage: jnm <message>"
return
}
jj new -m ($args -join " ")
}
function jm {
if ($args.Count -eq 0) {
Write-Error "Usage: jm <message>"
return
}
jj describe -m ($args -join " ")
}
function jcm {
if ($args.Count -eq 0) {
Write-Error "Usage: jcm <message>"
return
}
jj commit -m ($args -join " ")
}
function jci { jj commit @args }
function jdesc { jj describe @args }
function jme { jj metaedit @args }
function je { jj edit @args }
function jep { jj edit "@-" @args }
function jnext { jj next @args }
function jprev { jj prev @args }
function jrestore {
if ($args.Count -eq 0) {
Write-Error "Usage: jrestore <revision> [paths]. To discard the changes in @, use: jdiscard [paths]"
return
}
_jjRev restore '--from' $args
}
function jdiscard { jj restore @args }
function jrevert {
if ($args.Count -eq 0) {
Write-Error "Usage: jrevert <revision> [flags]"
return
}
$Placement = @()
if (-not (_jjHasFlag $args '-o', '--onto', '-d', '--destination', '-A', '--insert-after', '--after', '-B', '--insert-before', '--before')) {
$Placement = '--insert-before', '@'
}
_jjRev revert '-r' (@($args) + $Placement)
}
function ja { jj abandon @args }
# Shape history
function jsq { _jjRev squash '-r' $args }
function jsqi {
if ($args.Count -eq 0) {
Write-Error "Usage: jsqi <target-revision> [paths]"
return
}
_jjRev squash '--into' $args
}
function jsqp { _jjRev @('squash', '--interactive') '-r' $args }
function jsplit { _jjRev split '-r' $args }
function jabs { _jjRev absorb '--from' $args }
function jfix { _jjRev fix '-s' $args }
function jdupe {
# Duplicate (default @), then edit the copy when exactly one was made.
$Output = @(jj duplicate --color=never @args 2>&1 | ForEach-Object { "$_" })
$ExitCode = $LASTEXITCODE
$Output | ForEach-Object { Write-Host $_ }
if ($ExitCode -ne 0) { return }
$Copies = @()
foreach ($Line in $Output) {
if ($Line -match '^Duplicated \S+ as (\S+) ') { $Copies += $Matches[1] }
}
if ($Copies.Count -eq 1) {
jj edit $Copies[0]
}
elseif ($Copies.Count -gt 1) {
Write-Host "Made $($Copies.Count) copies and stayed on the current change. Edit one with: je CHANGE_ID"
}
else {
Write-Error "Could not find the new change in the output above. Edit it with: je CHANGE_ID"
}
}
function jrb {
if ($args.Count -eq 0) {
Write-Error "Usage: jrb <destination> [flags]"
return
}
_jjRev rebase '--onto' $args
}
function jrbs { _jjRebaseOnto '--source' 'Usage: jrbs <source> <destination> [flags]' $args }
function jrbr { _jjRebaseOnto '--revisions' 'Usage: jrbr <revisions> <destination> [flags]' $args }
function jarr { jj arrange @args }
function jpar { jj parallelize @args }
function jsimp { _jjRev simplify-parents '-r' $args }
function jdiffedit { _jjRev diffedit '-r' $args }
function jconv { jj converge @args }
function jrun {
# Leading flags are jj run options, and the first plain word starts the command.
$Options = @()
$Index = 0
while ($Index -lt $args.Count -and (_jjIsFlag $args[$Index])) {
if ($args[$Index] -ceq '--') { $Index++; break }
$Options += $args[$Index]
if ($args[$Index] -cin '-r', '--revision', '-j', '--jobs', '-R', '--repository') {
$Index++
if ($Index -lt $args.Count) { $Options += $args[$Index] }
}
$Index++
}
$Command = @($args | Select-Object -Skip $Index)
if ($Command.Count -eq 0) {
Write-Error "Usage: jrun [-r REVSET] [-j JOBS] <command> [arguments]"
return
}
# Plain `jj run` covers every mutable revision connected to @, so default to @ alone.
if (-not (_jjHasFlag $Options '-r', '--revision')) {
$Options = @('-r', '@') + $Options
}
jj run @Options -- @Command
}
function jrunstack { jrun -r "trunk()..@" @args }
function jbis { jj bisect @args }
# Conflicts
function jres { jj resolve @args }
function jresl { jj resolve --list @args }
# Bookmarks
function jb { jj bookmark @args }
function jbl { jj bookmark list @args }
function jbc { _jjNamed @('bookmark', 'create') '-r' $args }
function jbs { _jjNamed @('bookmark', 'set') '-r' $args }
function jbm { _jjNamed @('bookmark', 'move', '--allow-backwards') '--to' $args }
function jba { jj bookmark advance @args }
function jbd { jj bookmark delete @args }
function jbr { jj bookmark rename @args }
function jbt { jj bookmark track @args }
function jbu { jj bookmark untrack @args }
# Remote helpers: use the only configured remote unless one is named
function _jjGit {
$GitRoot = jj git root
if ($LASTEXITCODE -eq 0) {
git -C $GitRoot @args
}
}
function _jjResolveRemote {
param($Remote)
if ($Remote) { return $Remote }
$Remotes = @(
@(_jjGit remote) |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($LASTEXITCODE -ne 0) { return }
if ($Remotes.Count -eq 1) { return $Remotes[0] }
if ($Remotes.Count -eq 0) {
Write-Error "This repository has no Git remote."
return
}
Write-Error "This repository has multiple Git remotes. Pass the remote name explicitly."
}
function _jjLatest {
# @, or @- when @ is an empty change with no description (as right after jcm).
$Latest = jj log -r '@' --no-graph -T 'if(empty && description == "", "@-", "@")'
if ($LASTEXITCODE -ne 0) { return }
if ($Latest -eq '@-') {
Write-Host "Using @- because @ is empty and has no description."
}
$Latest
}
function jbdp {
param($Name, $Remote)
if (-not $Name) {
Write-Error "Usage: jbdp <bookmark> [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj bookmark delete "exact:$Name"
if ($LASTEXITCODE -eq 0) {
jj git push --bookmark "exact:$Name" --remote $ResolvedRemote @args
}
}
# Fetch, push, and publish
function jg { jj git @args }
function jgf { _jjRev @('git', 'fetch') '--remote' $args }
function jgfa { jj git fetch --all-remotes @args }
function jup {
jgf @args
if ($LASTEXITCODE -eq 0) {
jj rebase --onto "trunk()"
}
}
function jgp { _jjRev @('git', 'push') '--bookmark' $args }
function jgpp {
param($Bookmark, $Remote)
if (-not $Bookmark) {
Write-Error "Usage: jgpp <bookmark> [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj git push --bookmark "exact:$Bookmark" --remote $ResolvedRemote --dry-run
}
function jpub {
param($Bookmark, $Revision, $Remote)
if (-not $Bookmark) {
Write-Error "Usage: jpub <bookmark> [revision] [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
if (-not $Revision) { $Revision = _jjLatest }
if (-not $Revision) { return }
jj bookmark set $Bookmark --revision $Revision
if ($LASTEXITCODE -eq 0) {
jj git push --bookmark "exact:$Bookmark" --remote $ResolvedRemote
}
}
function jpc {
param($Revision, $Remote)
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
if (-not $Revision) { $Revision = _jjLatest }
if (-not $Revision) { return }
jj git push --change $Revision --remote $ResolvedRemote
}
function jgpa { jj git push --all @args }
function jgpforce {
param($Bookmark, $Remote)
if (-not $Bookmark) {
Write-Error "Usage: jgpforce <bookmark> [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj git export
if ($LASTEXITCODE -ne 0) { return }
_jjGit push --force $ResolvedRemote "refs/heads/${Bookmark}:refs/heads/${Bookmark}"
if ($LASTEXITCODE -eq 0) {
jj git fetch --remote $ResolvedRemote
}
}
# Git remotes
function jgra { jj git remote add @args }
function jgrr { jj git remote remove @args }
function jgrl { jj git remote list @args }
function jgrn { jj git remote rename @args }
function jgru { jj git remote set-url @args }
function jgroot { jj git root @args }
# Tags
function jt { jj tag @args }
function jtl { jj tag list @args }
function jtla { jj tag list --all-remotes @args }
function jtc { _jjNamed @('tag', 'set') '-r' $args }
function jtm { _jjNamed @('tag', 'set', '--allow-move') '-r' $args }
function jtd { jj tag delete @args }
function jtp {
param($Name, $Remote)
if (-not $Name) {
Write-Error "Usage: jtp <tag> [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj git push --tag "exact:$Name" --remote $ResolvedRemote @args
}
function jtsp {
param($Name, $Revision, $Remote)
if (-not $Name) {
Write-Error "Usage: jtsp <tag> [revision] [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
if (-not $Revision) { $Revision = _jjLatest }
if (-not $Revision) { return }
jj tag set $Name --revision $Revision
if ($LASTEXITCODE -eq 0) {
jtp $Name $ResolvedRemote @args
}
}
function jtdp {
param($Name, $Remote)
if (-not $Name) {
Write-Error "Usage: jtdp <tag> [remote]"
return
}
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj tag delete "exact:$Name"
if ($LASTEXITCODE -eq 0) {
jj git push --tag "exact:$Name" --remote $ResolvedRemote @args
}
}
function jtrl {
param($Remote)
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
_jjGit ls-remote --tags --refs $ResolvedRemote @args
}
function jtagmirror {
param($Remote)
$ResolvedRemote = _jjResolveRemote $Remote
if (-not $ResolvedRemote) { return }
jj git push --tag '*' --remote $ResolvedRemote --dry-run
if ($LASTEXITCODE -ne 0) { return }
$Confirm = Read-Host "Type yes to push these tag changes to '$ResolvedRemote'"
if ($Confirm -cne "yes") {
Write-Host "Cancelled."
return
}
jj git push --tag '*' --remote $ResolvedRemote
}
# Undo and operations
function jundo { jj undo @args }
function jredo { jj redo @args }
function jop { jj op log @args }
function jopshow { jj op show @args }
function jopdiff { jj op diff @args }
function joprevert { jj op revert @args }
function joprestore { jj op restore @args }
function jopclean {
param($Operation)
if (-not $Operation -or $args.Count -gt 0) {
Write-Error "Usage: jopclean <newest-operation-to-abandon>"
return
}
jj op show $Operation
if ($LASTEXITCODE -ne 0) { return }
Write-Host "`nThis will abandon operation '$Operation' and every ancestor." -ForegroundColor Yellow
Write-Host "Those states stop protecting old commits from garbage collection." -ForegroundColor Yellow
$Confirm = Read-Host "Type abandon to continue"
if ($Confirm -cne "abandon") {
Write-Host "Cancelled."
return
}
jj op abandon "..$Operation"
}
# Workspaces, sparse patterns, and files
function jws { jj workspace @args }
function jsparse { jj sparse @args }
function jfshow { jj file show @args }
function jflist { _jjRev @('file', 'list') '-r' $args }
function jfa { jj file annotate @args }
function jftrack { jj file track @args }
function jfuntrack { jj file untrack @args }
function jfchmod { jj file chmod @args }
# Signing, configuration, and utilities
function jsign { _jjRev sign '-r' $args }
function junsign { _jjRev unsign '-r' $args }
function jcfg { jj config @args }
function jcfgc { jj config gc @args }
function jgc { jj util gc @args }
function jutil { jj util @args }
function jclean {
param([Alias("y")][switch]$Yes)
$Revs = '(mine() & mutable()) ~ ::(bookmarks() | remote_bookmarks() | tags() | remote_tags() | working_copies())'
$Ids = @(jj log -r $Revs --no-graph -T 'commit_id ++ "\n"')
if ($LASTEXITCODE -ne 0) { return }
if ($Ids.Count -eq 0) {
Write-Host "No disconnected mutable changes authored by you."
return
}
jj log -r $Revs
Write-Host "`n$($Ids.Count) disconnected mutable change(s) found." -ForegroundColor Yellow
if ($Yes) {
jj abandon -r $Revs
return
}
$Confirm = Read-Host "Abandon these changes? [y/N]"
if ($Confirm -notmatch '^(y|yes)$') {
Write-Host "Cancelled."
return
}
jj abandon -r $Revs
}
function jroot { jj root @args }
function jcd {
$Root = jj root
if ($LASTEXITCODE -eq 0) {
Set-Location -LiteralPath $Root
}
}
function jcode {
$Root = jj root
if ($LASTEXITCODE -eq 0) {
code.cmd $Root @args
}
}
function jver { jj version @args }
function jh { jj help @args }Paste this into ~/.bashrc or ~/.zshrc, below the completion line from Set up your shell
# Pass-through and repository setup
j() { jj "$@"; }
ji() { jj git init --colocate "$@"; }
jclone() { jj git clone "$@"; }
# Argument handling shared by the shortcuts: leading plain values fill a shortcut's
# slots, flags go to jj unchanged, and defaults apply only when you did not type the
# matching flag yourself.
_jj_has_flag() {
# _jj_has_flag "FLAG..." ARGUMENTS...: succeed when an argument is one of the flags.
local flags=" $1 " argument
shift
for argument in "$@"; do
[[ "$flags" == *" ${argument%%=*} "* ]] && return 0
done
return 1
}
_jj_rev() {
# _jj_rev FLAG COMMAND... -- ARGUMENTS...: a leading plain value becomes FLAG VALUE.
local flag="$1"
shift
local -a jj_args=()
while [[ $# -gt 0 && "$1" != "--" ]]; do
jj_args+=("$1")
shift
done
[[ $# -gt 0 ]] && shift
if [[ $# -gt 0 && "$1" != -* ]]; then
jj_args+=("$flag" "$1")
shift
fi
jj "${jj_args[@]}" "$@"
}
_jj_range() {
# Like _jj_rev, but two leading plain values become --from A --to B.
local flag="$1"
shift
local -a jj_args=()
while [[ $# -gt 0 && "$1" != "--" ]]; do
jj_args+=("$1")
shift
done
[[ $# -gt 0 ]] && shift
if [[ $# -ge 2 && "$1" != -* && "$2" != -* ]]; then
jj_args+=(--from "$1" --to "$2")
shift 2
elif [[ $# -gt 0 && "$1" != -* ]]; then
jj_args+=("$flag" "$1")
shift
fi
jj "${jj_args[@]}" "$@"
}
_jj_named() {
# _jj_named FLAG COMMAND... -- ARGUMENTS...: NAME, then a plain value becomes FLAG VALUE.
local flag="$1"
shift
local -a jj_args=()
while [[ $# -gt 0 && "$1" != "--" ]]; do
jj_args+=("$1")
shift
done
[[ $# -gt 0 ]] && shift
if [[ $# -gt 0 && "$1" != -* ]]; then
jj_args+=("$1")
shift
if [[ $# -gt 0 && "$1" != -* ]]; then
jj_args+=("$flag" "$1")
shift
fi
fi
jj "${jj_args[@]}" "$@"
}
_jj_stack() {
# _jj_stack COMMAND... -- ARGUMENTS...: trunk()..REVISION, where a plain value replaces @.
local -a jj_args=()
while [[ $# -gt 0 && "$1" != "--" ]]; do
jj_args+=("$1")
shift
done
[[ $# -gt 0 ]] && shift
local revision=@
if [[ $# -gt 0 && "$1" != -* ]]; then
revision="$1"
shift
fi
jj "${jj_args[@]}" -r "trunk()..($revision)" "$@"
}
_jj_rebase_onto() {
# _jj_rebase_onto FLAG USAGE ARGUMENTS...: jj rebase FLAG FIRST --onto SECOND.
local flag="$1" usage="$2"
shift 2
if [[ $# -gt 0 && "$1" == -* ]]; then
jj rebase "$@"
return
fi
if [[ $# -lt 2 || "$2" == -* ]]; then
printf '%s\n' "$usage" >&2
return 2
fi
local first="$1" destination="$2"
shift 2
jj rebase "$flag" "$first" --onto "$destination" "$@"
}
# Status, logs, and diffs
js() { jj status "$@"; }
jl() { _jj_rev -r log -- "$@"; }
jla() { jj log -r "all()" "$@"; }
jll() {
local limit="${1:-20}"
shift || true
jj log --limit "$limit" "$@"
}
jlb() {
local bookmark="${1:-trunk()}"
shift || true
local limit="${1:-20}"
shift || true
jj log -r "ancestors($bookmark)" --limit "$limit" "$@"
}
jlbroot() {
local bookmark="${1:-trunk()}"
shift || true
jj log -r "root()::$bookmark" --reversed "$@"
}
jstack() { _jj_stack log -- "$@"; }
jreview() { _jj_stack show --reversed -- "$@"; }
jnow() { jj status && jstack; }
jmine() { jj log -r "mine() & mutable()" "$@"; }
jconf() { jj log -r "conflicts()" "$@"; }
jwip() { jj log -r "wip()" "$@"; }
jstale() { jj log -r "stale()" "$@"; }
jdiv() { jj log -r "divergent()" "$@"; }
jprivate() {
local private_revset
private_revset=$(jj config get git.private-commits) || return
jj log -r "$private_revset" "$@"
}
jsh() { jj show "$@"; }
jd() { _jj_range -r diff -- "$@"; }
jds() { _jj_range -r diff --stat -- "$@"; }
jfiles() { _jj_range -r diff --summary -- "$@"; }
jevo() { _jj_rev -r evolog -- "$@"; }
jidiff() { _jj_range --from interdiff -- "$@"; }
# Create, edit, and restore changes
jn() { jj new "$@"; }
jnm() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jnm MESSAGE\n' >&2
return 2
fi
jj new -m "$*"
}
jm() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jm MESSAGE\n' >&2
return 2
fi
jj describe -m "$*"
}
jcm() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jcm MESSAGE\n' >&2
return 2
fi
jj commit -m "$*"
}
jci() { jj commit "$@"; }
jdesc() { jj describe "$@"; }
jme() { jj metaedit "$@"; }
je() { jj edit "$@"; }
jep() { jj edit "@-" "$@"; }
jnext() { jj next "$@"; }
jprev() { jj prev "$@"; }
jrestore() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jrestore REVISION [PATHS]. To discard the changes in @, use: jdiscard [PATHS]\n' >&2
return 2
fi
_jj_rev --from restore -- "$@"
}
jdiscard() { jj restore "$@"; }
jrevert() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jrevert REVISION [FLAGS]\n' >&2
return 2
fi
if _jj_has_flag "-o --onto -d --destination -A --insert-after --after -B --insert-before --before" "$@"; then
_jj_rev -r revert -- "$@"
else
_jj_rev -r revert -- "$@" --insert-before @
fi
}
ja() { jj abandon "$@"; }
# Shape history
jsq() { _jj_rev -r squash -- "$@"; }
jsqi() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jsqi TARGET_REVISION [PATHS]\n' >&2
return 2
fi
_jj_rev --into squash -- "$@"
}
jsqp() { _jj_rev -r squash --interactive -- "$@"; }
jsplit() { _jj_rev -r split -- "$@"; }
jabs() { _jj_rev --from absorb -- "$@"; }
jfix() { _jj_rev -s fix -- "$@"; }
jdupe() {
# Duplicate (default @), then edit the copy when exactly one was made.
local output exit_code copies count
output=$(jj duplicate --color=never "$@" 2>&1)
exit_code=$?
[[ -n "$output" ]] && printf '%s\n' "$output"
[[ $exit_code -eq 0 ]] || return "$exit_code"
copies=$(printf '%s\n' "$output" | sed -nE 's/^Duplicated [^ ]+ as ([^ ]+) .*/\1/p')
count=$(printf '%s\n' "$copies" | awk 'NF { n++ } END { print n + 0 }')
case "$count" in
1) jj edit "$copies" ;;
0)
printf 'Could not find the new change in the output above. Edit it with: je CHANGE_ID\n' >&2
return 1
;;
*) printf 'Made %s copies and stayed on the current change. Edit one with: je CHANGE_ID\n' "$count" ;;
esac
}
jrb() {
if [[ $# -eq 0 ]]; then
printf 'Usage: jrb DESTINATION [FLAGS]\n' >&2
return 2
fi
_jj_rev --onto rebase -- "$@"
}
jrbs() { _jj_rebase_onto --source 'Usage: jrbs SOURCE DESTINATION [FLAGS]' "$@"; }
jrbr() { _jj_rebase_onto --revisions 'Usage: jrbr REVISIONS DESTINATION [FLAGS]' "$@"; }
jarr() { jj arrange "$@"; }
jpar() { jj parallelize "$@"; }
jsimp() { _jj_rev -r simplify-parents -- "$@"; }
jdiffedit() { _jj_rev -r diffedit -- "$@"; }
jconv() { jj converge "$@"; }
jrun() {
# Leading flags are jj run options, and the first plain word starts the command.
local -a run_options=()
while [[ $# -gt 0 && "$1" == -* ]]; do
if [[ "$1" == "--" ]]; then
shift
break
fi
run_options+=("$1")
case "$1" in
-r|--revision|-j|--jobs|-R|--repository)
shift
[[ $# -gt 0 ]] && run_options+=("$1")
;;
esac
[[ $# -gt 0 ]] && shift
done
if [[ $# -eq 0 ]]; then
printf 'Usage: jrun [-r REVSET] [-j JOBS] COMMAND [ARGUMENTS]\n' >&2
return 2
fi
# Plain `jj run` covers every mutable revision connected to @, so default to @ alone.
if ! _jj_has_flag "-r --revision" "${run_options[@]}"; then
run_options=(-r @ "${run_options[@]}")
fi
jj run "${run_options[@]}" -- "$@"
}
jrunstack() { jrun -r "trunk()..@" "$@"; }
jbis() { jj bisect "$@"; }
# Conflicts
jres() { jj resolve "$@"; }
jresl() { jj resolve --list "$@"; }
# Bookmarks
jb() { jj bookmark "$@"; }
jbl() { jj bookmark list "$@"; }
jbc() { _jj_named -r bookmark create -- "$@"; }
jbs() { _jj_named -r bookmark set -- "$@"; }
jbm() { _jj_named --to bookmark move --allow-backwards -- "$@"; }
jba() { jj bookmark advance "$@"; }
jbd() { jj bookmark delete "$@"; }
jbr() { jj bookmark rename "$@"; }
jbt() { jj bookmark track "$@"; }
jbu() { jj bookmark untrack "$@"; }
# Remote helpers: use the only configured remote unless one is named
_jj_git() {
local git_root
git_root=$(jj git root) || return
git -C "$git_root" "$@"
}
_jj_resolve_remote() {
local requested_remote="${1:-}"
if [[ -n "$requested_remote" ]]; then
printf '%s\n' "$requested_remote"
return
fi
local remote_output
remote_output=$(_jj_git remote) || return
local remote remote_count=0 sole_remote=
while IFS= read -r remote; do
if [[ -n "$remote" ]]; then
sole_remote="$remote"
remote_count=$((remote_count + 1))
fi
done <<<"$remote_output"
case "$remote_count" in
1) printf '%s\n' "$sole_remote" ;;
0)
printf 'This repository has no Git remote.\n' >&2
return 1
;;
*)
printf 'This repository has multiple Git remotes. Pass the remote name explicitly.\n' >&2
return 1
;;
esac
}
_jj_latest() {
# Print @, or @- when @ is an empty change with no description (as right after jcm).
local latest
latest=$(jj log -r @ --no-graph -T 'if(empty && description == "", "@-", "@")') || return
if [[ "$latest" == "@-" ]]; then
printf 'Using @- because @ is empty and has no description.\n' >&2
fi
printf '%s\n' "$latest"
}
jbdp() {
local name="${1:?Usage: jbdp NAME [REMOTE]}"
shift
local requested_remote="${1:-}"
if [[ $# -gt 0 ]]; then shift; fi
local remote
remote=$(_jj_resolve_remote "$requested_remote") || return
jj bookmark delete "exact:$name" &&
jj git push --bookmark "exact:$name" --remote "$remote" "$@"
}
# Fetch, push, and publish
jg() { jj git "$@"; }
jgf() { _jj_rev --remote git fetch -- "$@"; }
jgfa() { jj git fetch --all-remotes "$@"; }
jup() {
jgf "$@" &&
jj rebase --onto "trunk()"
}
jgp() { _jj_rev --bookmark git push -- "$@"; }
jgpp() {
local bookmark="${1:?Usage: jgpp BOOKMARK [REMOTE]}"
local remote
remote=$(_jj_resolve_remote "${2:-}") || return
jj git push --bookmark "exact:$bookmark" --remote "$remote" --dry-run
}
jpub() {
local bookmark="${1:?Usage: jpub BOOKMARK [REVISION] [REMOTE]}"
local revision="${2:-}"
local remote
remote=$(_jj_resolve_remote "${3:-}") || return
if [[ -z "$revision" ]]; then
revision=$(_jj_latest) || return
fi
jj bookmark set "$bookmark" --revision "$revision" &&
jj git push --bookmark "exact:$bookmark" --remote "$remote"
}
jpc() {
local revision="${1:-}"
local remote
remote=$(_jj_resolve_remote "${2:-}") || return
if [[ -z "$revision" ]]; then
revision=$(_jj_latest) || return
fi
jj git push --change "$revision" --remote "$remote"
}
jgpa() { jj git push --all "$@"; }
jgpforce() {
local bookmark="${1:?Usage: jgpforce BOOKMARK [REMOTE]}"
local remote
remote=$(_jj_resolve_remote "${2:-}") || return
jj git export &&
_jj_git push --force "$remote" \
"refs/heads/${bookmark}:refs/heads/${bookmark}" &&
jj git fetch --remote "$remote"
}
# Git remotes
jgra() { jj git remote add "$@"; }
jgrr() { jj git remote remove "$@"; }
jgrl() { jj git remote list "$@"; }
jgrn() { jj git remote rename "$@"; }
jgru() { jj git remote set-url "$@"; }
jgroot() { jj git root "$@"; }
# Tags
jt() { jj tag "$@"; }
jtl() { jj tag list "$@"; }
jtla() { jj tag list --all-remotes "$@"; }
jtc() { _jj_named -r tag set -- "$@"; }
jtm() { _jj_named -r tag set --allow-move -- "$@"; }
jtd() { jj tag delete "$@"; }
jtp() {
local name="${1:?Usage: jtp NAME [REMOTE]}"
local remote
remote=$(_jj_resolve_remote "${2:-}") || return
jj git push --tag "exact:$name" --remote "$remote"
}
jtsp() {
local name="${1:?Usage: jtsp NAME [REVISION] [REMOTE]}"
local revision="${2:-}"
local remote
remote=$(_jj_resolve_remote "${3:-}") || return
if [[ -z "$revision" ]]; then
revision=$(_jj_latest) || return
fi
jj tag set "$name" --revision "$revision" &&
jtp "$name" "$remote"
}
jtdp() {
local name="${1:?Usage: jtdp NAME [REMOTE]}"
local remote
remote=$(_jj_resolve_remote "${2:-}") || return
jj tag delete "exact:$name" &&
jj git push --tag "exact:$name" --remote "$remote"
}
jtrl() {
local remote
remote=$(_jj_resolve_remote "${1:-}") || return
_jj_git ls-remote --tags --refs "$remote"
}
jtagmirror() {
local remote
remote=$(_jj_resolve_remote "${1:-}") || return
jj git push --tag '*' --remote "$remote" --dry-run || return
printf "Type yes to push these tag changes to '%s': " "$remote"
local confirm
read -r confirm
if [[ "$confirm" != "yes" ]]; then
printf 'Cancelled.\n'
return
fi
jj git push --tag '*' --remote "$remote"
}
# Undo and operations
jundo() { jj undo "$@"; }
jredo() { jj redo "$@"; }
jop() { jj op log "$@"; }
jopshow() { jj op show "$@"; }
jopdiff() { jj op diff "$@"; }
joprevert() { jj op revert "$@"; }
joprestore() { jj op restore "$@"; }
jopclean() {
if [[ $# -ne 1 ]]; then
printf 'Usage: jopclean NEWEST_OPERATION_TO_ABANDON\n' >&2
return 2
fi
local operation="$1"
jj op show "$operation" || return
printf "\nThis will abandon operation '%s' and every ancestor.\n" "$operation"
printf 'Those states stop protecting old commits from garbage collection.\n'
printf 'Type abandon to continue: '
local confirm
read -r confirm
if [[ "$confirm" != "abandon" ]]; then
printf 'Cancelled.\n'
return
fi
jj op abandon "..$operation"
}
# Workspaces, sparse patterns, and files
jws() { jj workspace "$@"; }
jsparse() { jj sparse "$@"; }
jfshow() { jj file show "$@"; }
jflist() { _jj_rev -r file list -- "$@"; }
jfa() { jj file annotate "$@"; }
jftrack() { jj file track "$@"; }
jfuntrack() { jj file untrack "$@"; }
jfchmod() { jj file chmod "$@"; }
# Signing, configuration, and utilities
jsign() { _jj_rev -r sign -- "$@"; }
junsign() { _jj_rev -r unsign -- "$@"; }
jcfg() { jj config "$@"; }
jcfgc() { jj config gc "$@"; }
jgc() { jj util gc "$@"; }
jutil() { jj util "$@"; }
jclean() {
local assume_yes=false
case "${1:-}" in
-y|--yes)
assume_yes=true
shift
;;
esac
if [[ $# -ne 0 ]]; then
printf 'Usage: jclean [-y|--yes]\n' >&2
return 2
fi
local revs='(mine() & mutable()) ~ ::(bookmarks() | remote_bookmarks() | tags() | remote_tags() | working_copies())'
local count
count=$(jj log -r "$revs" --no-graph -T 'commit_id ++ "\n"' |
awk 'NF { count++ } END { print count + 0 }') || return
if [[ "$count" -eq 0 ]]; then
printf 'No disconnected mutable changes authored by you.\n'
return
fi
jj log -r "$revs" || return
printf '\n%s disconnected mutable change(s) found.\n' "$count"
if [[ "$assume_yes" == true ]]; then
jj abandon -r "$revs"
return
fi
printf 'Abandon these changes? [y/N] '
local confirm
read -r confirm
case "$confirm" in
y|Y|yes|YES) jj abandon -r "$revs" ;;
*) printf 'Cancelled.\n' ;;
esac
}
jroot() { jj root "$@"; }
jcd() {
local root
root=$(jj root) || return
cd "$root" || return
}
jcode() {
local root
root=$(jj root) || return
code "$root" "$@"
}
jver() { jj version "$@"; }
jh() { jj help "$@"; }