Majutsu is a Magit-inspired Emacs interface for the Jujutsu (jj) version control system. It provides a powerful, interactive log viewer and a comprehensive set of commands for manipulating revision history with the efficiency and comfort Emacs users expect.
Majutsu aims to bring the legendary workflow of Magit to the Jujutsu (jj) version control system. It is not merely a wrapper around the jj CLI, but a deeply integrated Emacs environment that leverages Magit’s section management and transient menu systems. Majutsu provides visual tools for complex operations like rebasing, squashing, and conflict resolution, making the power of Jujutsu accessible and intuitive.
Originally started as a fork of jj-mode.el, Majutsu has been heavily refactored and expanded to include a new template DSL, asynchronous process handling, and a native Evil-mode integration.
Jujutsu (jj) is a next-generation version control system that is both simple and powerful. It features a unique “working copy is a commit” model (@), first-class support for mutable history, and robust conflict management. Unlike Git, jj does not have a staging area; instead, changes in the working copy are automatically recorded into the current revision. This makes it an ideal companion for Emacs, where buffers are often in a state of flux.
Majutsu stands on the shoulders of giants. We are grateful to:
- The Jujutsu community for creating a revolutionary VCS.
- The Magit authors and contributors for defining the gold standard of VCS interfaces.
- Brandon Olivier for the initial codebase of
jj-mode.el.
- Emacs 29.1 or later.
- Jujutsu (jj) v0.39.0 or later installed and in your
PATH. magit3.3.0 or later (for section management).transient0.5.0 or later (popup menus).with-editor(for descriptive message editing).
Majutsu is currently available via GitHub. You can install it using your preferred Emacs package manager.
Add the following to your packages.el:
(package! majutsu :recipe (:host github :repo "0WD0/majutsu"))(use-package majutsu
:straight (:host github :repo "0WD0/majutsu"))Emacs 29+ users can use the built-in package-vc support:
(use-package majutsu
:vc (:url "https://github.com/0WD0/majutsu"))Clone the repository and add it to your load-path:
(add-to-list 'load-path "/path/to/majutsu")
(require 'majutsu)If you use Evil mode, Majutsu includes a native integration that provides sensible Vim-like keybindings. It is enabled by default if Evil is detected. You can customize this via M-x customize-group RET majutsu-evil RET.
To begin using Majutsu, navigate to a directory within a Jujutsu repository and run M-x majutsu-log (or its alias majutsu).
If the directory is not a repository, Majutsu will offer to initialize one using jj git init. Once the log buffer opens, you will see a graphical representation of your revision history, similar to Magit’s log but with Jujutsu’s unique features like hidden and divergent revisions clearly marked.
Navigation is straightforward: use n and p to move between revisions, and RET to visit the revision at point. Press ? at any time to open the Dispatcher, which shows all available commands.
Majutsu uses several specialized buffer types to provide a rich interface.
The primary interface for Majutsu. It displays the revision graph, working copy status, and active workspaces. It uses majutsu-log-mode, derived from magit-section-mode.
Displays changes between revisions or within the working copy. It supports Magit-style hunk and file sections, word-level refinement, and interactive patching.
Allows viewing the contents of a file at a specific revision. You can navigate through the file’s history using n and p within the blob buffer.
Shows the output of background jj commands. If a command fails, you can press $ to inspect the error.
Used for writing commit descriptions or other interactive input. It uses with-editor to ensure seamless integration with the Emacs environment.
JJ descriptions buffers highlight metadata blocks and change summaries. You can customize majutsu-jjdescription-major-mode, majutsu-jjdescription-comment-prefix, majutsu-jjdescription-change-id-face, and global-majutsu-jjdescription-mode to taste. JJ comment commands use JJ: as the comment prefix. JJ: ignore-rest marks the remainder of the buffer as comment. Other major modes are not thoroughly tested yet. Jujutsu currently does not support changing the comment prefix; the option is reserved for future compatibility.
Majutsu organizes information into collapsible sections. You can use TAB to toggle the visibility of a section (e.g., a revision’s description in the log, or a file in a diff).
Commands in Majutsu are grouped into “transient” menus. These popups allow you to select options and flags before executing a command. For example, pressing r opens the Rebase transient.
For commands like Rebase, Squash, or Absorb, Majutsu uses a visual selection system. You can mark “source” and “destination” revisions directly in the log buffer, and they will be highlighted with distinct colors until the operation is executed or cleared.
Majutsu integrates with Emacs’ completion system (like Vertico or Ivy) for selecting bookmarks, remotes, and revsets. Revset completion candidates are annotated with source labels (pseudo/workspace/bookmark/tag) to make ambiguous names easier to identify. Destructive operations like abandon or undo will prompt for confirmation.
Majutsu runs jj commands asynchronously whenever possible to keep the Emacs UI responsive. Large log graphs or remote operations won’t freeze your editor.
The log buffer is the heart of Majutsu. It displays the history graph using a custom template DSL that mirrors the jj log output but adds interactivity.
Each revision in the log is a section. The visible anchor line is built from fields assigned to the heading module plus any auxiliary fields assigned to the tail module in majutsu-log-commit-columns (for example, author and timestamp). Graph prefixes and graph-related indentation are rendered as display-only prefix decoration rather than real buffer text, so they do not get copied accidentally and point does not land on them as editable text. Collapsed by default, you can expand a revision to see foldable body module content (for example, long descriptions).
Press l to open the log options. Here you can:
r- Set a revset filter (e.g.,
all()ormine()). -n- Limit the number of revisions shown.
-v- Toggle reverse order.
-G- Toggle the ASCII graph.
--- Add fileset/path filters to limit the log to matching files.
While you can type revsets manually, Majutsu’s selection system allows you to build them interactively. Commands that require a revset will often default to the revision at point or your current selection.
Majutsu’s log parser uses an ordered marker protocol embedded in jj template output:
- Entry start:
\x1dS - Tail start:
\x1dT - Body start:
\x1dB - Metadata start:
\x1dM - Entry end:
\x1dE
Within each module payload, fields are separated by \x1e.
Protocol control bytes are reserved:
\x1dfor module markers\x1efor field boundaries\x1ffor encoded logical newlines
Avoid emitting these bytes literally in user templates unless intentionally participating in the protocol.
Newline rules:
- heading module: may contain real physical newlines.
- tail/body/metadata modules: stay in the trailing payload; encode logical newlines as
\x1fand decode them in postprocessing.
Parsing is strictly sequential and positional (not key/value based):
- Detect
\x1dSand lock the entry indent column. - Collect heading lines until
\x1dTis found. - Parse
T -> B -> M -> Epayload segments in order on the final heading line (no extra physical line breaks between these module markers). - Keep lines between current entry
Eand next entrySattached to the current section heading area (graph continuation lines).
Each item in majutsu-log-commit-columns is a plist:
:field- Field symbol mapped to a
majutsu-log-template-<field>variable. :module- One of
heading,tail,body,metadata. :face- Highlight policy for that field.
:post- Optional postprocessor function (or function list).
Only these keys are used by the current parser/renderer.
Majutsu ensures hidden metadata fields id, commit-id, and parent-ids are always available even if omitted from user configuration.
heading- Visible anchor-line content on the left (can span physical lines before the tail segment begins).
tail- Single-line auxiliary content rendered on the anchor line with right alignment. It remains searchable as real text, but copying a mixed heading+tail region drops the tail text by default; copying the tail alone preserves it.
- Rendered log spans are tagged with
majutsu-log-module,majutsu-log-field,majutsu-log-column,majutsu-log-entry-id, andmajutsu-log-decorationso explicit copy commands can distinguish content from graph/tail decorations. - Hidden transport fields always include
id,commit-id, andparent-ids, even if you omit them from visible layout. That keeps stable identity, relation navigation, and commit-hash copying available.
body- Foldable section body.
metadata- Parsed/stored for behavior and lookup; not directly shown by default.
t- Preserve jj-provided text properties/highlighting.
nil- Strip text properties (plain string).
FACE-SYMBOL- Repaint field using that face.
If :face is omitted, Majutsu preserves jj-provided text properties (t).
:post can be:
- A function symbol
- A list of functions (applied left-to-right)
- Omitted or
:default(uses default postprocessors) nil(disable column postprocessors)
Function contract:
(fn VALUE &optional CTX) => NEW-VALUECTX includes at least :field, :module, and the normalized :column
spec.
Postprocessors run per column instance, so the same field can appear in
multiple modules and project to different values. Transport decoding (for
example, turning \x1f back into \n) happens before :post runs.
When you provide a function or function list in :post, Majutsu appends it
after the field’s default postprocessors.
(setq majutsu-log-commit-columns
'((:field change-id :module heading :face t)
(:field description :module heading :face t)
(:field author :module tail :face magit-log-author)
(:field timestamp :module tail :face magit-log-date)
(:field long-desc :module body :face t)
(:field id :module metadata :face nil)
(:field commit-id :module metadata :face nil)
(:field flags :module metadata :face nil)));; heading field can emit real newlines directly
(setq majutsu-log-template-description
[:concat [:change_id] "\n" [:description]])
;; non-heading field should encode line breaks as \x1f
(setq majutsu-log-template-long-desc
[:description :lines :skip 1 :join "\x1f"])(defun my-log-trim (value &optional _ctx)
(if (stringp value) (string-trim-right value) value))
(setq majutsu-log-commit-columns
'((:field description :module heading :face t :post my-log-trim)
(:field id :module metadata :face nil)))Older column keys like :align and :visible are no longer part of the log column schema.
The historical right-margin placement has been replaced by the tail module, which renders real text on the anchor line with right alignment.
Move fields between visual areas by changing :module, and adjust display behavior with :face and :post. In log buffers, copying a mixed heading+tail selection strips the tail text by default, while copying the tail itself preserves it. Graph prefixes and graph-related indentation are display-only, so ordinary copy operations only capture semantic content rather than the visible graph gutter.
Majutsu now provides both default copy filtering and explicit semantic copy commands:
M-x majutsu-copy-section-value/? w s- copy the current section’s stable value (for
jj-commit, this is the revision id at point). If the region is active, it falls back to ordinary region copy. M-x majutsu-log-copy-field/? w f- copy the rendered field value at point.
M-x majutsu-log-copy-entry-field/? w F- choose any canonical field stored on the current entry, including hidden metadata that is not visible in the current layout.
M-x majutsu-log-copy-commit-id/? w h- copy the current entry’s commit hash from hidden metadata.
M-x majutsu-log-copy-module/? w m- copy the rendered visible module at point (
heading,tail, orbody), without graph-prefix or tail-spacer decoration.
This keeps ordinary Emacs copying predictable while still offering precise log-aware copy operations when you want semantic rather than purely visual text.
Press d to open the Diff transient.
r- Select revisions to diff (
--revisions). Can specify multiple revisions or a revset likeB::D. f/t- Select the
--fromand--torevisions. --- Limit diff to selected files/filesets.
-g- Toggle Git-style diff output.
-W- Toggle color-words diff output.
-S- Toggle stat output.
-s- Toggle summary output.
-c- Set context line count.
-b/-w- Ignore whitespace amount / ignore all whitespace.
The --revisions argument accepts any revset that forms a contiguous set of commits. “Contiguous” means no gaps in the DAG, but forks and merges are allowed.
Examples:
-r @- Changes in the working copy commit (default).
-r @-- Changes in the parent of the working copy.
-r B::D- Total changes from B through D.
-r 'A | B'- Valid only if A and B form a contiguous set; gaps produce an error.
When visiting a file from such a diff:
- On an added or context line: visits
heads(revisions). - On a removed line: visits
roots(revisions)-(the parents of roots).
Note: When the revset has multiple heads or roots, the target revision for file visits may be ambiguous. For example, in an X-shaped history:
D E <- two heads \ / C / \ A B <- two roots
If you diff -r A::D | B::E, there are two heads (D, E) and two roots (A, B). The diff shows changes from the merged parents to the merged heads. If D and E modify the same lines, the diff will show conflict markers. When visiting files, Majutsu picks one of the heads/roots, which may not be the specific commit where the displayed change originated. For precise navigation, use --from / --to with single revisions instead.
The diff buffer is highly interactive:
RET- Visit the appropriate version of the file at point. For working copy diffs, added/context lines visit the workspace file while removed lines visit the parent-side blob. For committed changes, it visits the blob at the corresponding side.
C-j/C-<return>- Visit the workspace file, regardless of diff type. Note: when Evil mode is active,
C-jmay be overridden by Evil’s section navigation; useC-<return>instead, which is unaffected. +/-- Increase or decrease the amount of context shown.
t- Toggle word-level refinement.
When --color-words is enabled, Majutsu renders the old/new line numbers in
the left margin and uses “…” lines to split hunks.
With refinement enabled, color-words hunks also show a shadow cursor on the
paired side (controlled by smerge-refine-shadow-cursor).
Navigation (RET / C-j) still respects the side at point by using stored
line/column metadata from the color-words backend.
Majutsu’s file inspector is implemented by majutsu-file and can be used both
from command prompts and with defaults derived from the current section context.
M-x majutsu-find-file- Prompt for revset + path (defaulting from point when possible) and open that file as a blob.
M-x majutsu-find-file-other-window- Like
majutsu-find-file, but display the blob in another window. M-x majutsu-find-file-other-frame- Like
majutsu-find-file, but display the blob in another frame.
Blob buffers are read-only snapshots with history-aware navigation:
p/n- Jump to previous/next revision that touched this file (while preserving cursor position as much as possible).
V- Jump to the workspace version of the same file.
C-c m- Open the current blob in Magit’s blob viewer.
b- Start annotate for the current blob.
g- Revert/reload the current blob content.
e/C-x C-q- Enter editable blob mode (wdired-style).
- Blob navigation keys are disabled while editing, so blob-mode bindings do not interfere with text edits.
- Save (or
C-c C-c) applies changes through non-interactivejj diffeditand exits edit mode. Majutsu copies current buffer text into the diffedit right side and finishes automatically, without opening the right-side temp file buffer. - In editable mode,
C-x C-qrunsmajutsu-blob-edit-exit:- If modified, it prompts to save or abort.
- Save applies via diffedit; abort restores original content and cursor position.
- If unchanged, it exits immediately.
- In Evil:
- In blob mode,
ienters editable blob mode and stays in normal state. - In editable mode,
ienters insert state.
- In blob mode,
- Editable mode also changes cursor visuals (see
majutsu-blob-edit-cursor-type); with Evil, normal-state cursor is updated too.
Majutsu annotate is implemented by majutsu-annotate and uses
jj file annotate under the hood.
- Entry: run
b(majutsu-annotate-addition) from a blob/file buffer. - Recursive trace: currently only forward recursive annotate is supported.
Running
bagain on an annotated chunk jumps to its parent revision (when available) and re-annotates there. - Styles:
majutsu-annotate-cycle-styleis available. In default read-only annotate bindings, presscto cycle heading/highlight/line styles (majutsu-annotate-styles). - Navigation:
n/pfor next/previous chunk,N/Pfor next/previous chunk from the same commit. - Inspection:
RETshows the chunk’s revision diff,M-wcopies the chunk change-id, andSPC/S-SPCscroll the diff window if it is already open.
Press E in dispatcher to open majutsu-ediff transient.
- Selection: choose revisions via
--revisionsor--from/--to(with point-toggle variants). In diff buffers, current range is used as default.
e(majutsu-ediff-dwim)- Compare based on context (hunk/file/commit/whole buffer).
E(majutsu-ediff-edit)- Run
jj diffeditwith Emacs asui.diff-editor. If no file is at point, Majutsu prompts for a changed file, then launches a two-sided Ediff session for that single file (left/right temp files). Edit the right-side temp file and quit Ediff to return control tojj diffedit. Whenmajutsu-edit-finish-on-saveis non-nil, saving a diffedit temp file can finish the with-editor session automatically.
Resolve entry points:
- Ediff transient:
m(majutsu-ediff-resolve),M(majutsu-ediff-resolve-with-conflict). - Direct commands:
M-x majutsu-ediff-resolve,M-x majutsu-ediff-resolve-with-conflict.
m(majutsu-ediff-resolve)- Resolve conflicted files.
In a
jj-commitsection, it lists conflicted files for that revision; otherwise it uses working copy@. Usesjj resolvewith 3-way Ediff (merge tool:$left,$base,$right) for up-to-2-sided conflicts, and falls back tojj diffeditfor conflicts with more than 2 sides. In this flow, quitting Ediff without editing leaves jj’s output unchanged, so the conflict stays unresolved. If you edited and quit, Majutsu asks whether to save the resolved result; choosing no discards edits and keeps conflicts. When only part of a conflict is resolved, remaining regions are written with git-style conflict markers so jj keeps unresolved regions unresolved. M(majutsu-ediff-resolve-with-conflict)- Open the resolve target
buffer (working-copy file or revision blob), enable
majutsu-conflict-mode, and jump to the first conflict.
Press $ (Emacs) or ` (Evil) to see the output of the last jj command. This is essential for debugging failed operations or viewing long-running output like remote fetches.
Jujutsu tracks every operation that modifies the repository. Press M-x majutsu-op-log to view this history. You can see when commands were run and by whom.
O- Create a new change on top of the revision at point (DWIM).
o- Open the New transient to specify options.
A- Create a new change after the revision at point.
B(Emacs),I(Evil)- Create a new change before the revision at point.
Transient options:
-r- Parent revisions.
-A- Insert after constraint.
-B- Insert before constraint.
-m- Set a message.
-e- No edit (don’t switch to the new change).
Runs: jj new -r REV
c(majutsu-describe)- Edit the description of the revision at point. Supports
--ignore-immutableflag for immutable revisions.
Runs: jj describe -r REV
C(majutsu-commit)- In Jujutsu, “commit” usually means finishing the current work. This command opens a description buffer for the working copy.
Runs: jj commit
e(majutsu-edit-changeset)- Move the working copy (@) to the revision at point. Supports
--ignore-immutableflag for immutable revisions.
Runs: jj edit REV
s(majutsu-squash)- Open the Squash transient. You can select multiple source revisions (
--from) to squash into a destination (--into). -r- Revision to squash.
-f- Source revisions (
--from). -t- Destination revision (
--into). -o- Onto destination.
-A/-B- Insert after/before constraints.
--- Limit squash to specific filesets.
-k- Keep commits that become empty after squashing.
Runs: jj squash --from SRC --into DEST [FILESETS...]
a(Emacs/Evil), or?thenA(majutsu-absorb)- Open the Absorb transient.
-f- Source revision (
--from, default@). -t- Destination revset filter (
--into, defaultmutable()). --- Limit absorb to specific filesets.
Runs: jj absorb --from REV --into REVSET [FILESETS...]
r(majutsu-rebase)- Open the Rebase transient. This is one of Majutsu’s most powerful features, allowing you to visually select sources and destinations.
-s- Rebase a specific revision.
-b- Rebase an entire branch.
-o- Specify the destination (
--onto).
Runs: jj rebase -s SRC -o DEST
y(majutsu-duplicate)- Open the Duplicate transient. Allows selecting source revisions and destination.
Y(majutsu-duplicate-dwim)- Duplicate the revision at point onto its current parent.
-r- Source revisions to duplicate.
-o- Destination (
--onto). -A/-B- Insert after/before constraints.
Runs: jj duplicate -r REV
k(Emacs),x(Evil)- Abandon the revision at point. Its changes are lost, and its descendants are rebased. With an active region selection, Majutsu abandons all selected revisions.
Runs: jj abandon -r REV
- Emacs:
C-//C-? - Undo / Redo
- Evil:
u/C-r - Undo / Redo
Runs: jj undo or jj redo
S(majutsu-split)- Open the Split transient. This allows you to split a revision into multiple parts.
-r- Specify the revision to split.
-o- Specify the destination (
--onto). -A/-B- Insert after/before constraints.
-m- Set a message for the first part.
-p- Parallel split mode.
--- Limit split to selected files/filesets.
Runs: jj split -r REV [FILESETS...]
R(majutsu-restore)- Open the Restore transient for undoing changes.
-f- Restore from a specific revision (
--from). -t- Restore to a specific revision (
--to). -c- Undo changes introduced by a revision (
--changes-in). -d- Restore descendants as well.
--- Limit restore to selected files/filesets.
Runs: jj restore --from REV [FILESETS...] or jj restore --changes-in REV [FILESETS...]
V(Emacs),_(Evil) (majutsu-revert)- Open the Revert transient to create reverse changes in a new revision.
-r- Source revisions to revert (
--revisions). -o- Apply reverse changes on top of revisions (
--onto). -A/-B- Insert reverse changes after/before selected revisions.
Runs: jj revert --revisions REV --onto DEST
>(majutsu-sparse)- Open the sparse working copy transient (set, add, remove, list, edit).
Sparse transient actions:
l- List current sparse patterns in
*Majutsu Sparse*buffer. s/S- Set patterns (append / replace with clear first).
a/r- Add or remove patterns incrementally.
e- Edit patterns via
jj sparse edit(withmajutsu-jjsparse-mode). R- Reset to all files (
jj sparse reset).
Notes:
- Editing patterns opens a temporary
.jjsparsebuffer. - Lines starting with
JJ:are comments and are ignored by Jujutsu. - Pattern completion includes current directories/files from
@. - Default “all files” state is represented by single pattern
..
Majutsu provides Magit-style partial hunk selection for Jujutsu operations. This allows you to select specific hunks, files, or even regions within hunks to include in Split, Squash, or Restore operations.
Interactive selection is integrated into the Split (S), Squash (s), and Restore (R) transients. When you open one of these transients from a Diff buffer, a “Patch Selection” group appears with the following commands:
| Key | Command | Description |
|---|---|---|
H | majutsu-interactive-toggle-hunk | Toggle selection of hunk at point |
F | majutsu-interactive-toggle-file | Toggle selection of all hunks in file |
R | majutsu-interactive-toggle-region | Toggle selection of active region |
C | majutsu-interactive-clear | Clear all patch selections |
When opening Split, Squash, or Restore from a Diff buffer, the transient automatically inherits the diff’s context:
- All three commands inherit
--revisionsas their target revision (--revisionfor Split/Squash) - Restore additionally inherits
--fromand--toparameters, allowing selective restoration between arbitrary revisions
This means you can:
- View a diff with specific
--from/--torevisions - Open the Restore transient
- Select specific hunks to restore
- The restore will apply only to those hunks, using the diff’s revision context
Selected hunks are highlighted with majutsu-interactive-selected-hunk face (green background by default). Selected regions within hunks use majutsu-interactive-selected-region face (purple background).
The meaning of “selected” differs by operation:
For Split and Squash, selected content is what gets moved:
- Split: Selected hunks/regions go into the first commit; unselected content stays in the second commit.
- Squash: Selected hunks/regions get squashed into the parent; unselected content remains in the current revision.
Example: You have a revision with changes to files A, B, and C. You want to squash only the changes to file A into the parent:
- Open the diff for the revision (
D) - Open Squash transient (
s) - Press
Fon file A to select all its hunks - Execute squash - file A’s changes go to parent, B and C stay
For Restore, selected content is what gets restored (undone):
- Selected hunks/regions are reverted to their state in the source revision
- Unselected content is left unchanged
Majutsu uses a custom merge tool to apply partial patches. When you execute an operation with selections:
- Patch Generation: Majutsu generates a unified diff patch containing only the selected hunks/regions.
- Tool Invocation: Jujutsu’s
-i --toolmechanism is used with a custommajutsu-applypatchtool. - Patch Application:
- For Split/Squash: The tool resets
$right(current state) to$left(parent state), then applies the patch forward. This results in$rightcontaining only the selected changes. - For Restore: The tool applies the patch directly to
$right.
- For Split/Squash: The tool resets
This approach avoids the complexity of reverse patch application (git apply -R), which has edge cases with new files, deleted files, and content starting with + or -.
Interactive patching supports all file operation types:
When splitting or squashing a new file:
- If you select the entire file, it goes to the first commit / gets squashed
- If you select only part of the file, only those lines go; the rest stays
- Partial selection of new files works correctly because the patch is applied forward after resetting to parent state
Note: Interactive selection for deleted files is currently limited.
When splitting or squashing a file deletion:
- Selecting the entire deletion works correctly - the file gets deleted in the first commit / parent
- Partial selection of deleted files is not yet supported - you must select all lines or none
This limitation exists because git’s patch format requires deleted file mode
patches to remove the entire file contents. Partial deletion would require
converting to a regular modification patch, which has edge cases with context
matching.
Renamed and copied files are handled correctly:
- The rename/copy metadata is preserved in the patch
- You can select specific hunks within renamed files just like regular modifications
- Open a diff with
Dord - Open Split (
S), Squash (s), or Restore (R) transient - Use
Hto select individual hunks, orFto select all hunks in a file - For fine-grained control, mark a region and press
Rto select only those lines - Press
Cto clear selections if needed - Execute the operation - only selected changes will be affected
majutsu-interactive-selected-hunk- Face for selected hunks
majutsu-interactive-selected-region- Face for selected regions
?thenm(majutsu-metaedit)- Open the Metaedit transient. Defaults to one revision from point (fallback
@). If multiple revisions are selected, Majutsu asks you to narrow it down. -r/-m/-a/-t- Set revision, message, author, and author timestamp.
-c/-u/-U/-f- Toggle update-change-id / update-author / update-author-timestamp / force-rewrite.
-I- Ignore immutable revisions.
Runs: jj metaedit --revisions REV [OPTIONS...]
?thenP(majutsu-simplify-parents)- Open the Simplify Parents transient.
-s- Simplify specified revision(s) together with descendant trees (
--source). -r- Simplify specified revision(s) only (
--revisions). If neither is set, Majutsu defaults to region/point revision(s), fallback@. -I- Ignore immutable revisions.
Runs: jj simplify-parents [--source REVSET] [--revisions REVSET]
In Jujutsu, bookmarks are similar to Git branches but are explicitly tracked. They point to a specific change ID.
Press b to manage bookmarks.
l- List all bookmarks.
c- Create a new bookmark at point.
s- Set (move) an existing bookmark to point to the current revision.
m- Move a bookmark.
M- Move bookmark(s) with
--allow-backwards. r- Rename a bookmark.
d- Delete a bookmark (this deletion propagates to remotes).
f- Forget a bookmark (removes local tracking without affecting remotes).
t- Track a remote bookmark to create a local one.
u- Untrack a remote bookmark.
In Jujutsu, tags are lightweight refs that point to revisions, similar to Git tags.
Majutsu wraps jj tag set/list/delete and follows jj semantics where creating and
moving tags both use tag set (moving requires --allow-move).
Tag commands are intentionally dispatch-first. Open dispatcher with ?, then
press t to manage tags.
l- List tags in a dedicated buffer.
s- Set tag(s). Uses completion and allows new names.
m- Move existing tag(s) to another revision (internally
--allow-move). d- Delete tag(s) by name/pattern.
Tag prompts use completion candidates from local tags. Existing tags are shown for selection, while set/delete prompts still allow entering string patterns manually.
Press G (Emacs) / ? G (Evil) to access Git-specific commands. Jujutsu can interact directly with Git remotes.
p- Open the Push transient. You can push bookmarks, revisions, or changes.
-a- Push all bookmarks.
-t- Push tracked bookmarks only.
-b- Push specific bookmark(s).
-c- Push a specific change.
-r- Push specific revision(s).
-y- Show what would be pushed without pushing (
--dry-run).
Runs: jj git push
f- Open the Fetch transient to pull changes from a remote.
-R- Fetch from a specific remote.
-B- Fetch a specific branch.
-t- Fetch only tracked bookmarks.
-A- Fetch from all remotes.
Runs: jj git fetch
r- Open the Remote Management transient.
l- List remotes.
a- Add a new Git remote.
d- Remove a remote.
n- Rename a remote.
u- Set remote URL.
c- Clone a Git repository into a jj repo.
i- Initialize a new Git-backed jj repository.
Jujutsu automatically exports/imports to the underlying Git repo, but you can trigger it manually:
ejj git exportmjj git importo- Show Git directory path (
jj git root)
Jujutsu supports multiple workspaces sharing the same repository storage. This is similar to Git worktrees but more integrated.
Press Z or % (Emacs) / * (Evil) to manage workspaces.
l- List all workspaces.
v- Visit a workspace (only changes default-directory, and refresh majutsu buffers).
a- Add a new workspace.
f- Forget a workspace.
u- Update a stale workspace.
n- Rename a workspace.
r- Show and copy current workspace root.
Workspace sections are rendered from a structured jj workspace list -T
template. By default they show each workspace name, working-copy ids,
description, and resolved root path when available. If jj cannot resolve a
workspace root, Majutsu treats it as unavailable instead of showing the raw
<Error: ...> text in the section body.
JJ marks conflicts in the log with a “conflict” label. Expanding the revision will show which files are conflicted.
You can enter conflict handling from multiple places:
- In Ediff transient, use
m(auto strategy) orM(force conflict workflow).Mopens the working-copy file (or a revision blob buffer for non-working-copy revisions), enablesmajutsu-conflict-mode, and jumps to the first conflict. - Manually, run
M-x majutsu-conflict-ensure-modein a file with conflict markers.
majutsu-conflict auto-detects marker styles:
- JJ conflict markers (
%%%%%%%/+++++++/-------) -> enablemajutsu-conflict-mode. - JJ long conflict markers (e.g., 15-char marker runs) are treated the same as normal markers.
- JJ conflicts with missing terminating newline markers are parsed with jj’s newline-compensation semantics.
- Git-style markers (
|||||||/ =======) only -> enablesmerge-mode.
Default key prefix is C-c ^:
n/p- Jump to next/previous conflict.
b- Keep conflict base (snapshot / rebase destination section).
1..9- Keep side N (“after” side in JJ diff blocks).
M-1..M-9- Keep side N “before” variant.
R- Refine conflict regions with word-level highlighting.
In Evil integration:
]]/[[orgj/gk- Next/previous conflict.
gb- Keep base.
grandgR- Resolve “after” / “before” side maps.
ge- Refine.
majutsu-jj-executable- Path to the
jjbinary (default:"jj"). majutsu-jj-global-arguments- Global arguments passed to all jj commands (default:
("--no-pager" "--color=always")). majutsu-jj-diffstat-columns- Column width used only for
jj diff --stat(default:80). Set tonilto inherit terminal width. Raise it if you want more full path visibility; keep it near 80 if you prefer compact stat bars. majutsu-show-process-buffer-hint- Whether to show the “Type $ for details” hint on errors.
majutsu-debug- Enable debug logging for jj operations.
majutsu-show-command-output- Show jj command output in messages.
majutsu-confirm-critical-actions- If non-nil, prompt for confirmation before critical operations like abandon, undo, redo, rebase.
majutsu-no-confirm- A list of symbols for actions Majutsu should not confirm, or
tto never confirm. Valid symbols:undo,redo,abandon,rebase,workspace-forget. majutsu-slow-confirm- A list of actions that should use
yes-or-no-pinstead ofy-or-n-p.
majutsu-process-popup-time- Popup the process buffer if a command takes longer than this many seconds.
-1means never,0means immediately. majutsu-process-log-max- Maximum number of sections to keep in a process log buffer (default: 32).
majutsu-process-apply-ansi-colors- When non-nil, convert ANSI escapes in jj output to text properties.
majutsu-process-timestamp-format- Format string for timestamps in process buffer sections.
majutsu-jj-environment- Extra
KEY=VALUEentries prepended to subprocess environments for all jj invocations (local and TRAMP), applied centrally bymajutsu-process.
majutsu-log-commit-columns- Ordered log field layout for log rendering. It also controls face policy per field using
:face. For protocol details, module semantics, and schema examples, see Inspecting -> Log Buffer. majutsu-log-template-<field>variables- Template forms consumed by log fields. For newline and control-character rules, see Inspecting -> Log Buffer.
majutsu-log-sections-hook- Hook run to insert sections in the log buffer.
majutsu-display-buffer-function- Magit-style display strategy (default:
majutsu-display-buffer-traditional; traditional, same-window-except-diff, fullframe, topleft, fullcolumn). majutsu-pre-display-buffer-hook- Hook run before displaying a buffer.
majutsu-post-display-buffer-hook- Hook run after displaying a buffer.
majutsu-bury-buffer-function- Function used by
qto bury/quit Majutsu buffers (default:majutsu-mode-quit-window; alternatives includequit-windowandmajutsu-restore-window-configuration).
majutsu-diff-refine-hunk- Whether to show word-granularity differences inside hunks.
nildisables,trefines current hunk,'allrefines all hunks. majutsu-diff-refine-ignore-whitespace- Whether to ignore whitespace while refining hunks.
majutsu-diff-refine-max-chars- Skip word refinement when a hunk spans more than this many characters.
majutsu-diff-paint-whitespace- Whether to highlight whitespace issues inside diff hunks.
majutsu-diff-highlight-trailing- Whether to mark trailing whitespace in diff hunks.
majutsu-create-buffer-hook- Normal hook run when a new Majutsu buffer is created.
majutsu-setup-buffer-hook- Normal hook run after displaying the buffer.
majutsu-post-create-buffer-hook- Normal hook run after the initial refresh.
majutsu-refresh-buffer-hook- Normal hook run after refreshing.
majutsu-jjdescription-major-mode- Major mode used for editing JJ description buffers (default:
text-mode). majutsu-jjdescription-comment-prefix- Comment prefix used in JJ description buffers (default:
"JJ:"). majutsu-jjdescription-change-id-face- Face used for JJ Change ID values.
global-majutsu-jjdescription-mode- Global minor mode to enable JJ description buffer enhancements.
majutsu-evil-enable-integration- Set to
nilto disable automatic Evil bindings. majutsu-evil-initial-state- The Evil state to start in (default:
normal).
Majutsu includes a domain-specific language (DSL) for building Jujutsu templates in Emacs Lisp. The main entry point is majutsu-tpl, which compiles a vector-based DSL form into a jj template string.
The DSL provides several advantages over writing raw template strings:
- Compile-time validation: Syntax errors are caught during byte-compilation rather than at runtime when jj executes the template.
- Automatic escaping: String literals are properly escaped (quotes, backslashes, control characters) without manual intervention.
- Elisp integration: Embed Elisp expressions that evaluate at compile time, enabling dynamic template generation based on configuration or context.
- Composability: Define reusable template functions with
majutsu-template-defunthat expand inline, avoiding runtime overhead. - Type awareness: The DSL understands jj’s type system, enabling self-type context for cleaner keyword syntax (
[:description]instead of[:method [:self] :description]). - Readability: Vector-based syntax with keywords is more readable than deeply nested string concatenation.
Example comparison:
;; Raw string (error-prone, hard to read)
"if(self.root(), \"(root)\", self.commit_id().short())"
;; DSL (validated, composable, readable)
(majutsu-tpl [:if [:root] "(root)" [:commit_id :short]] 'Commit)Vectors without a leading keyword are implicitly concatenated:
(majutsu-tpl ["A" "B"]) ; => "concat(\"A\", \"B\")"
(majutsu-tpl [:concat "A" "B"]) ; => "concat(\"A\", \"B\")"Bare strings inside vectors are automatically treated as string literals (:str).
Use :str for explicit string literals with proper escaping:
(majutsu-tpl [:str "Hello \"World\""]) ; => "Hello \"World\""Use :raw to inject template code directly without escaping:
(majutsu-tpl [:raw "self.commit_id().short()"]) ; => "self.commit_id().short()"Untyped raw snippets now default to semantic type Unknown rather than
pretending to be Template. If you want method-chain typing to keep flowing,
add an explicit annotation:
[:raw "self" :Commit]The callable position of :call can be written as a quoted symbol,
keyword, or string name:
(majutsu-tpl [:call 'coalesce [:str ""] [:str "X"]]) ; => "coalesce(\"\", \"X\")"Elisp t and nil map to true and false. Numbers pass through directly:
(majutsu-tpl [:if t "yes" "no"]) ; => "if(true, \"yes\", \"no\")"
(majutsu-tpl [:call 'pad_end 8 "x"]) ; => "pad_end(8, \"x\")"Use :method to call methods on objects. Methods can be chained:
(majutsu-tpl [:method [:raw "commit" :Commit] :commit_id])
; => "commit.commit_id()"
(majutsu-tpl [:method [:raw "commit" :Commit] :parents :len])
; => "commit.parents().len()"
(majutsu-tpl [:method [:raw "commit" :Commit] :diff "src"])
; => "commit.diff(\"src\")"At public compile entry points, when a self type is provided (or the default
self type is configured), Majutsu installs a root self binding and bare
keywords become method calls on that receiver:
(majutsu-tpl [:description] 'Commit) ; => "self.description()"
(majutsu-tpl [:parents :len] 'Commit) ; => "self.parents().len()"Use [:self] when you need the current implicit receiver as a value. When
nested lambdas or helper-local :bind-self scopes introduce new receivers,
[:self N] selects the outer binding N levels up ([:self 0] is the same
as [:self]):
(majutsu-tpl [:method [:self] :description] 'Commit)
; => "self.description()"
(majutsu-tpl [:method [:raw "self" :Commit]
:parents
:map
[:|p| [:method [:self 1] :description]]])
; => "self.parents().map(|p| self.description())"Ordinary helpers can temporarily rebind the implicit self inside their body
with :bind-self:
(majutsu-template-defun show-canonical-log-id ((object Commit :optional t))
(:returns Template :bind-self object)
[:canonical-log-id])When the bound parameter is nil, the helper inherits the outer self binding.
Arithmetic and logical operators are supported:
(majutsu-tpl [:+ 1 2]) ; => "(1 + 2)"
(majutsu-tpl [:and [:> 3 1] [:<= 2 2]]) ; => "((3 > 1) && (2 <= 2))"
(majutsu-tpl [:not t]) ; => "(!true)"
(majutsu-tpl [:++ "L" "R"]) ; => "(\"L\" ++ \"R\")"(majutsu-tpl [:if [:root] "(root)" [:commit_id]])
; => "if(self.root(), \"(root)\", self.commit_id())"
(majutsu-tpl [:separate " " [:label "a" "A"] [:label "b" "B"]])
; => "separate(\" \", label(\"a\", \"A\"), label(\"b\", \"B\"))"Elisp expressions are evaluated when the template is compiled.
(let* ((tmp 1)
(s1 `[:concat ,(if (> 2 tmp) [:str "T"] [:str "F"]) [:str "!"]])
(s2 [:concat (if (> 2 tmp) [:str "T"] [:str "F"]) [:str "!"]])
(tmp 3))
(concat (majutsu-tpl s1) (majutsu-tpl s2)))
; => "concat(\"T\", \"!\")concat(\"F\", \"!\")"Anonymous functions are first-class template values:
(majutsu-tpl [:lambda [c] [:description]])
; => "|c| c.description()"
(majutsu-tpl [:|c| [:description]])
; => "|c| c.description()"
(majutsu-tpl [:parents :map [:|c| [:description]]])
; => "self.parents().map(|c| c.description())"
(majutsu-tpl [:call [:|c| [:description]] [:raw "item" :Commit]])
; => "item.description()"
(majutsu-tpl [[:lambda [c] [:description]] [:raw "item" :Commit]])
; => "item.description()"The shorthand [:|c| BODY] is equivalent to [:lambda [c] BODY].
Lambda parameters are lexical variables and can act as a deferred implicit self
for bare keyword dispatch. A surface lambda such as [:lambda [c] [:description]]
is therefore kept generic first and can later be specialized from a typed call
argument or a higher-order container element. In those cases [:description]
becomes equivalent to [:method 'c :description]. If you prefer, you can also
write that explicit [:method 'c ...] form directly, but most examples use bare
keywords because it better matches the implicit-self model. Explicit lexical
references also keep working across nested lambdas, so inner bodies may still
refer to outer parameters by name when that is clearer than rebinding receiver
context. This deferred behavior is limited to lambda parameters; Majutsu no
longer uses unknown non-lambda receivers as a general bare-keyword fallback. If
you need an outer receiver explicitly inside a nested lambda, use [:self N].
For example, the inner body below uses all three forms at once: [:description]
for the inner receiver, [:self 1] for the outer receiver, and explicit
[:method 'o ...] for the outer lexical parameter:
(majutsu-tpl
[[:|o|
[[:|i|
[:if [:method 'o :root]
[:description]
[:method [:self 1] :description]]]
[:raw "inner" :Commit]]]
[:raw "outer" :Commit]])
; => "if(outer.root(), inner.description(), outer.description())"List-oriented methods can be written in several equivalent styles:
;; Historical explicit-binder sugar (lowers to a native lambda internally)
(majutsu-tpl [:map [:raw "self.bookmarks()"] b [:raw "b.name()"]])
; => "self.bookmarks().map(|b| b.name())"
;; Direct jj-style method call with an anonymous lambda
(majutsu-tpl [:method [:raw "refs"] :map [:lambda [c] [:description]]])
; => "refs.map(|c| c.description())"
;; Dash-style explicit lambda
(majutsu-tpl [:-map [:lambda [c] [:description]] [:raw "refs"]])
; => "refs.map(|c| c.description())"
;; Dash-style anaphoric shorthand
(majutsu-tpl [:--map [:method 'it :description] [:raw "refs"]])
; => "refs.map(|it| it.description())"
(majutsu-tpl [:method
[:map [:raw "self.parents()"] p [:raw "p.commit_id()"]]
:join [:str ", "]])
; => "self.parents().map(|p| p.commit_id()).join(\", \")"
(majutsu-tpl [:map-join [:str ", "] [:raw "self.parents()"] p [:raw "p.commit_id()"]])
; => "self.parents().map(|p| p.commit_id()).join(\", \")"Prefer direct lambdas and map + join composition in new code. The historical
binder forms such as [:map collection var body] and :map-join remain as
compatibility sugar, but they now lower through the same core
:method + :lambda path as direct method calls. Similarly, :-map remains a
value-level explicit-lambda helper while the :--map family is syntax sugar
layered on top of the same lambda support.
This is a deliberate Majutsu DSL adaptation. Upstream jj centers the
higher-order story around expressions such as collection.map(|x| body);
Majutsu keeps that core model and layers additional sugar on top of it.
Reusable lambda bodies can therefore also be defined as ordinary helpers that
return native lambda values:
(majutsu-template-defun description-fn ()
(:returns Lambda)
[:lambda [c] [:description]])
(majutsu-tpl [:description-fn])
; => "|c| c.description()"
(majutsu-tpl [:method [:raw "refs"] :map [:description-fn]])
; => "refs.map(|c| c.description())"
(majutsu-template-defun description-with-suffix ((suffix Template))
(:returns Lambda)
`[:lambda [c] [:concat [:description] ,suffix]])
(majutsu-tpl [:method [:raw "refs"] :map [:description-with-suffix [:str "!"]]])
; => "refs.map(|c| concat(c.description(), \"!\"))"Use majutsu-template-defun to define reusable template functions:
(majutsu-template-defun my-helper ((label Template) (value Template :optional t))
(:returns Template)
`[:concat ,label [:str ": "] ,(or value [:str ""])])
(majutsu-tpl [:my-helper [:str "ID"] [:str "VAL"]])
; => "concat(\"ID\", \": \", \"VAL\")"When a helper omits its body, majutsu-template-defun defaults to a simple
wrapper around the same jj callable name:
(majutsu-template-defun my-fill ((width Integer) (content Template))
(:returns Template))
(majutsu-tpl [:my-fill 8 "x"])
; => "my-fill(8, \"x\")"Syntax-level sugar should be defined separately with
majutsu-template-defspecial, which receives raw forms and lowers them to more
primitive template syntax:
(majutsu-template-defspecial :wrap-angle (body)
`[:concat [:str "<"] ,body [:str ">"]])
(majutsu-tpl [:wrap-angle [:str "x"]])
; => "concat(\"<\", \"x\", \">\")"Owner-bound methods can also be defined locally in the DSL. A body-less
majutsu-template-defmethod / majutsu-template-defkeyword declaration just
registers metadata for a native/rendered method, but providing a body turns it
into a local owner-bound lowering:
(majutsu-template-defkeyword canonical-log-id Commit
(:returns Template)
[:if [:or [:hidden]
[:divergent]]
[:commit_id :shortest 8]
[:change_id :shortest 8]])
(majutsu-tpl [:canonical-log-id] 'Commit)
; => "if((self.hidden() || self.divergent()), self.commit_id().shortest(8), self.change_id().shortest(8))"
(majutsu-tpl [:method [:raw "p" :Commit] :canonical-log-id])
; => "if((p.hidden() || p.divergent()), p.commit_id().shortest(8), p.change_id().shortest(8))"The DSL supports Jujutsu’s type system: Any, String, Boolean, Integer,
Template, Commit, Signature, Timestamp, List, Option, and more.
Type annotations can be added to :raw nodes:
[:raw "self" :Commit] ; Declares the raw value has type CommitWithout an annotation, :raw remains Unknown. This keeps the DSL honest:
raw snippets still compile, but richer type propagation only kicks in once the
expression is explicitly typed or inferred through later semantic steps.
Majutsu distinguishes between broad Any expression placeholders and the
narrower printable Template capability. In practice, Any means “some
expression”, while Template means content that can actually be rendered or
concatenated.
Majutsu also propagates core result types through the normalized AST. For
example, parents() is tracked as a list of commits, lines() as a list of
strings, trailers() as (:list Trailer), mapped lists use ordinary
container refs such as (:list String), and list methods such as first()
preserve the element type.
A few upstream categories are still intentionally simplified in Majutsu’s
checker. For parameters that upstream models as StringLiteral, Majutsu
currently recognizes obvious literal strings, but it does not try to prove that
more complex expressions become literals after helper expansion or constant
folding. Upstream also has distinct AnyList-style result categories; Majutsu
currently models those as ordinary container refs instead.
Templates can be used with these jj commands:
| Command | Self Type |
|---|---|
| log | Commit |
| show | Commit |
| evolog | CommitEvolutionEntry |
| diff | TreeDiffEntry |
| bookmark list | CommitRef |
| tag list | CommitRef |
| file annotate | AnnotationLine |
| file list | TreeEntry |
| file show | TreeEntry |
| op log | Operation |
| op show | Operation |
| workspace list | WorkspaceRef |
| config list | ConfigValue |