Skip to content

Latest commit

 

History

History
2153 lines (1851 loc) · 98 KB

File metadata and controls

2153 lines (1851 loc) · 98 KB

Majutsu User Manual

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.

Introduction

About Majutsu

Majutsu is a Magit-inspired Emacs interface for Jujutsu. It provides an interactive log viewer and commands for inspecting and rewriting revision history. It uses Magit’s section and transient systems and provides visual workflows for operations such as rebasing, squashing, and conflict resolution.

About Jujutsu

Jujutsu (jj) is a version control system whose working copy is represented by a commit. Unlike Git, it has no staging area: the working-copy commit is marked @, and jj snapshots filesystem changes into it at the beginning of most commands. Mutable history and conflicts are first-class parts of Jujutsu’s model, making rebasing, squashing, and conflict resolution ordinary operations.

Acknowledgments

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.

Installation

Requirements

  • Emacs 29.1 or later.
  • compat 31.0.0.1 or later.
  • Jujutsu (jj) v0.41.0 or later installed and in your PATH.
  • magit 4.4.0 or later (for section management).
  • consult 1.0 or later and plz 0.9.1 or later (for native Gerrit completion).
  • transient 0.7.0 or later (popup menus).
  • with-editor (for descriptive message editing).

Installing from Git

Majutsu is currently available via GitHub. You can install it using your preferred Emacs package manager.

Doom Emacs

Add the following to your packages.el:

(package! majutsu :recipe (:host github :repo "0WD0/majutsu"))

use-package with straight.el

(use-package majutsu
  :straight (:host github :repo "0WD0/majutsu"))

use-package :vc (Emacs 30+)

The built-in use-package :vc keyword is available in Emacs 30 and later. Use :rev :newest to follow the repository’s current default branch:

(use-package majutsu
  :vc (:url "https://github.com/0WD0/majutsu"
       :rev :newest))

package-vc (Emacs 29+)

Emacs 29 added built-in package-vc support. Install Majutsu directly, then configure it with use-package:

(unless (package-installed-p 'majutsu)
  (package-vc-install
   '(majutsu :url "https://github.com/0WD0/majutsu")))

(use-package majutsu)

Manual Installation

Clone the repository and add it to your load-path:

(add-to-list 'load-path "/path/to/majutsu")
(require 'majutsu)

Post-Installation Tasks

Vanilla: No keymap setup is required; Majutsu’s native bindings are active as soon as its modes load.

Evil: Majutsu detects Evil, installs its normal-state integration by default, and opens Majutsu buffers in the configured Evil state. Customize this with M-x customize-group RET majutsu-evil RET.

Getting Started

Start from a file or Dired buffer whose default-directory is inside an existing Jujutsu repository, then run M-x majutsu-log (the shorter M-x majutsu is an alias). With a prefix argument, or when the current directory is outside a repository, Majutsu asks which directory to use. If that directory is not a repository either, it asks before running jj git init; declining leaves the directory unchanged.

The log buffer is the safest place to learn the interface. Find the working copy revision, marked @.

Vanilla: Use n and p to move by revision section. TAB expands or collapses the section at point, while [ and ] move to a visible parent or child.

Evil: Majutsu starts the log in Normal state. Use C-j and C-k to move by section; g j and g k move between sibling sections. TAB expands or collapses the section at point, while [ and ] move to a visible parent or child.

Point is significant: commands opened from the log use the revision under point as their default. In particular, RET on a revision runs jj edit; it changes which revision the workspace edits, so do not use it merely to inspect a revision.

To inspect without running a history-rewriting command, put point on @ and press D (majutsu-diff-dwim). The resulting diff buffer shows the current change.

Vanilla: TAB folds files or hunks, n and p move through sections, and q returns to the log.

Evil: TAB folds files or hunks, C-j and C-k move through sections, and q returns to the log without leaving Normal state.

For an explicit selection, press d in the log to open the Diff transient. Its -r/r entry has two paths: -r reads a revset, while r toggles the revision at point into the --revisions selection. Press d to execute the diff, or c to clear the selection first. This does not invoke a rewrite command, although jj may snapshot changed working-copy files before producing output. Selections only become arguments when an action is executed.

Use ? to open the dispatcher and see the commands valid in the current buffer.

Vanilla: A safe first loop is: move through the log, inspect a diff, return with q, and refresh with g. If a jj command fails, press $ to open the process buffer and read its complete output.

Evil: A safe first loop is: move through the log in Normal state, inspect a diff, return with q, and refresh with g r. If a jj command fails, press ` to open the process buffer and read its complete output.

Only after this loop is familiar should you move on to commands in Manipulating, which can rewrite repository history and may ask for confirmation.

Interface Concepts

Buffers and Modes

Majutsu uses several specialized buffer types to provide a rich interface.

Log Buffer

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.

Diff Buffer

Displays changes between revisions or within the working copy. It supports Magit-style hunk and file sections, word-level refinement, and interactive patching.

Blob Buffer

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.

Process Buffer

Vanilla: Press $ in a Majutsu buffer to inspect the current repository’s recent synchronous and asynchronous jj commands, including failures and long-running remote operations.

Evil: Press ` in Normal state for the same per-repository command history, without taking over Evil’s $ motion.

JJ description Buffer

JJ descriptions are edited in an edit session. In the background, jj waits for the editor, usually emacsclient via with-editor, to save the description file and return. If the editor exits with a non-zero status, then jj aborts the operation. The most important commands are therefore those for finishing or canceling the edit session.

Key: C-c C-c (with-editor-finish)
Finish the current edit session successfully. jj then uses the description found in the file.
Key: C-c C-k (with-editor-cancel)
Cancel the current edit session with a non-zero editor status, causing jj to abort the operation. Majutsu saves the current text to its in-session description history before canceling.

JJ-generated comment blocks are highlighted using commit-message faces. Lines beginning with JJ: are treated as comments, and JJ: ignore-rest marks the remainder of the buffer as comment. You can customize majutsu-jjdescription-major-mode, majutsu-jjdescription-comment-prefix, majutsu-jjdescription-change-id-face, and global-majutsu-jjdescription-mode. Jujutsu currently does not support changing the comment prefix; the option exists for future compatibility.

Descriptions are also stored in a ring that lives for the duration of the Emacs session. Majutsu saves the current description when an edit session starts, and again when it finishes or is canceled, so older descriptions can be recovered easily.

Key: C-c M-s (majutsu-jjdescription-save-message)
Save the current buffer contents to the description ring.
Key: M-p (majutsu-jjdescription-prev-message)
Cycle backward through the description ring, after saving the current description to the ring. With a numeric prefix ARG, go back ARG entries.
Key: M-n (majutsu-jjdescription-next-message)
Cycle forward through the description ring, after saving the current description to the ring. With a numeric prefix ARG, go forward ARG entries.
Key: C-c M-p (majutsu-jjdescription-search-message-backward)
Search backward through the description ring for a substring match.
Key: C-c M-n (majutsu-jjdescription-search-message-forward)
Search forward through the description ring for a substring match.
Key: C-c C-d (majutsu-jjdescription-show-diff)
Show the diff for the described change. If the buffer contains a JJ: Change ID: line, then use that revision; otherwise fall back to @.

Sections

Majutsu organizes information into collapsible sections. TAB toggles the section at point, such as a revision description in the log or a file in a diff.

Vanilla: n and p move forward and backward through sections.

Evil: C-j and C-k move forward and backward through sections in Normal, Visual, and Motion states; g j and g k move between siblings.

Transient Menus

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.

Primary action entries, such as executing a rebase, squash, split, or upload, also use majutsu-transient-default-action (default RET). When a primary action has its own command-specific key, the transient displays the command key and the default action key on the same row and either key invokes the action.

Some transients support saved defaults. Global defaults follow Transient’s normal storage; repository-local defaults use jj’s secure repo config-id as the stable project key, so they survive moving the workspace. Where available, W saves the current transient arguments as defaults for the current jj repository only. This is intended for project-shaped UI policy such as log filters, diff format, and Git sync remotes, not for one-off destructive targets.

Visual Selection System

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.

Selection entries that support both manual input and point toggling show both keys on one row, for example -r/r: press the option key to read a revset manually, or the unprefixed key to toggle the revision at point. Majutsu runs these readers, point toggles, and primary actions in the buffer that opened the transient, so repository context, point defaults, and buffer-local selections stay consistent while the transient popup has focus.

Completion and Confirmation

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. Relation navigation keeps revision ids as the actual candidates and shows descriptions as annotations; structured-row field selection likewise keeps field names canonical while showing value previews as annotations. Destructive operations like abandon or undo will prompt for confirmation.

Running JJ

Majutsu records command output in a process buffer for each repository. Git push/fetch, Gerrit upload, diffedit, and commands waiting for with-editor run asynchronously. Log/diff rendering and most ordinary local mutations wait for jj to finish before the initiating Emacs command returns; the synchronous side-effect runner still services subprocesses such as an Emacs-based GPG pinentry while it waits.

Inspecting

Use this page to understand the log: how revision rows are rendered, how point and relation navigation work, and how to copy structured fields. The separate Files, Diffs, and Operations page continues from a selected revision into diffs, blobs, annotations, Ediff, process output, and Jujutsu’s operation log.

Log Buffer

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.

Log Display

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. When a revision has body module content (for example, continuation lines from a long description), that content forms the foldable section body; its current visibility follows the section state instead of being forced closed on every refresh.

Press [ to jump to a visible parent and ] to jump to a visible child. When more than one target is visible, completion returns the canonical revision id while displaying its description as an annotation.

Log Options Transient

Key: l (Vanilla) (majutsu-log-transient)
Open the log options transient.
Key: L (Evil) (majutsu-log-transient)
Open the log options transient.

Useful transient entries include:

Key: -r
Set a revset filter with Majutsu’s standard revset expression reader (e.g., all(), mine(), or A | B). The current filter is prefilled for editing; submit an empty value to clear it.
Key: -n
Limit the number of revisions shown.
Key: -v
Toggle reverse order.
Key: -G
Toggle the ASCII graph.
Key: –
Add fileset/path filters to limit the log to matching files.
Key: W
Save the current log arguments as defaults for this jj repository.

Revision Selection

Majutsu’s transient selection system chooses revision / change values; it is not a general revset builder. For repeatable arguments, an active log region adds each selected commit. Otherwise, when point is in a semantic revision section, the section’s value is used. Only outside such a section does the toggle fall back to literal revision syntax under point and then the surrounding buffer context.

For arguments that accept a single value, only one selected revision is used. Majutsu does not convert a region into an expression such as A..B or synthesize revset operators and functions. Enter such expressions directly with the normal revset reader, for example, the log transient’s -r entry. When no explicit value is selected, each command applies its own documented fallback.

Log Output Protocol (Sequential Markers)

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:

  • \x1d for module markers
  • \x1e for field boundaries
  • \x1f for 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 \x1f and decode them in postprocessing.

Parsing is strictly sequential and positional (not key/value based):

  1. Detect \x1dS and lock the entry indent column.
  2. Collect heading lines until \x1dT is found.
  3. Parse T -> B -> M -> E payload segments in order on the final heading line (no extra physical line breaks between these module markers).
  4. Keep lines between current entry E and next entry S attached to the current section heading area (graph continuation lines).

majutsu-log-commit-columns

majutsu-log-commit-columns is a flat list of column plists. Each plist declares one field occurrence in one row module.

:field
Required field symbol.
:module
Required; one of heading, tail, body, or metadata.
:template
Required Majutsu template form, or a bound template variable.
:face
Optional highlighting policy; defaults to t.
:post
Optional postprocessor function, function list, :default, or nil.

A field may occur in different modules, but the same field/module pair may occur only once. :instance is reserved for compiler-generated occurrence ids.

Only metadata occurrences populate canonical entry fields. The default columns therefore include metadata occurrences for id, commit-id, parent-ids, flags, and description. Preserve the fields needed by stable section identity, relation navigation and annotations, and hash copying when replacing the entire column list.

Module Semantics

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.
body
Foldable section body.
metadata
Parsed/stored for behavior and lookup; not directly shown by default.

Row Metadata

Rendered row spans are tagged with majutsu-row-module, majutsu-row-field, majutsu-row-column, majutsu-row-entry-id, and majutsu-row-decoration so explicit copy commands can distinguish content from graph/tail decorations.

Hidden transport fields exist only when declared as metadata columns. The default list includes canonical metadata for identity, navigation, annotations, and copying; replacing the whole list also replaces those guarantees.

Face Policy (:face)

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).

Postprocessing (:post)

: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-VALUE

CTX 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.

Configuration Examples

Minimal columns

(setopt majutsu-log-commit-columns
        '((:field change-id :module heading
           :template majutsu-log-template-change-id :face t)
          (:field description :module heading
           :template majutsu-log-template-description :face t)
          (:field author :module tail
           :template majutsu-log-template-author :face magit-log-author)
          (:field timestamp :module tail
           :template majutsu-log-template-timestamp :face magit-log-date)
          (:field long-desc :module body
           :template majutsu-log-template-long-desc :face t)
          (:field id :module metadata
           :template majutsu-log-template-id :face nil)
          (:field commit-id :module metadata
           :template [:commit_id] :face nil)
          (:field parent-ids :module metadata
           :template majutsu-log-template-parent-ids :face nil)
          (:field flags :module metadata
           :template majutsu-log-template-flags :face nil)
          (:field description :module metadata
           :template [:description :first_line] :face nil)))

Keep heading multiline + encoded body multiline

;; heading field can emit real newlines directly
(setopt majutsu-log-template-description
        [:concat [:change_id] "\n" [:description]])

;; non-heading field should encode line breaks as \x1f
(setopt majutsu-log-template-long-desc
        [:description :lines :skip 1 :join "\x1f"])

Custom postprocessor

(defun my-log-trim (value &optional _ctx)
  (if (stringp value) (string-trim-right value) value))

(setopt majutsu-log-commit-columns
        (mapcar
         (lambda (column)
           (if (and (eq (plist-get column :field) 'description)
                    (eq (plist-get column :module) 'heading))
               (plist-put (copy-sequence column) :post #'my-log-trim)
             column))
         majutsu-log-commit-columns))

Copying From Structured Row Buffers

majutsu-log-mode, majutsu-evolog-mode, and majutsu-op-log-mode share these copy commands through majutsu-row-copy-transient.

Key: ? w s (majutsu-copy-section-value)
Copy the current section value. An active region uses ordinary region copy.
Key: ? w f (majutsu-row-copy-field)
Copy the rendered field value at point.
Key: ? w F (majutsu-row-copy-entry-field)
Choose any canonical field stored on the current entry, including hidden metadata that is not visible in the current layout. Completion keeps the field name as the candidate and displays a one-line value preview as its annotation.
Key: ? w m (majutsu-row-copy-module)
Copy the rendered visible module at point (heading, tail, or body), without graph-prefix or tail-spacer decoration.

This keeps ordinary Emacs copying predictable while still offering precise row-aware copy operations when you want semantic rather than purely visual text.

Files, Diffs, and Operations

Start here after choosing a revision in the log. These inspection workflows share the repository and revision context of the buffer that opened them, but some editing and recovery actions are explicitly identified below.

Diffing

Diff Transient

Key: d (majutsu-diff)
Open the Diff transient.

Useful transient entries include:

Key: r
Select revisions to diff (--revisions). Can specify multiple revisions or a revset like B::D.
Key: f / t
Select the --from and --to revisions.
Key: –
Limit diff to selected files/filesets. Completion shows full repository-relative paths changed by the active --revisions or --from / --to selection. Without an explicit selection it uses revisions in the active log region, then the revision at point, and finally @. Fileset expressions remain free-form and can still be entered directly.
Key: -g
Toggle Git-style diff output.
Key: -W
Toggle color-words diff output.
Key: -S
Toggle stat output.
Key: -s
Toggle summary output.
Key: -c
Set context line count.
Key: -b / -w
Ignore whitespace amount / ignore all whitespace.
Key: W
Save the current diff arguments as defaults for this jj repository.

Understanding --revisions

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, Majutsu uses the exact range stored by the diff buffer:

  • On an added or context line: resolve the --revisions expression itself.
  • On a removed line: resolve the parents of that expression (REVSET- in revset syntax).

Blob buffers require that the chosen side resolve to exactly one revision. Majutsu reports an error rather than arbitrarily choosing one head or root.

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 can show conflict markers. Inspection of the combined diff still works, but visiting a blob from either ambiguous side fails the single-revision check. Use --from / --to with single revisions when precise file navigation matters.

Diff Buffer

The diff buffer is highly interactive:

For a diff of exactly one change, Majutsu displays collapsible revision headers and the change description above the diff. This includes the default working-copy diff and an explicit -r or --revisions revset that resolves to one change. The metadata is omitted for --from / --to ranges and revsets that resolve to zero or multiple changes. Bookmark labels use structured jj template fields available in every supported jj version, so remote-only bookmarks and names containing spaces, sigils, or @ are preserved. Control characters are escaped when rendered to keep the collapsible heading on one line.

Key: RET (majutsu-diff-visit-file)
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.
Key: C-j / C-<return> (Vanilla) (majutsu-diff-visit-workspace-file)
Visit the workspace file, regardless of diff type.
Key: C-<return> (Evil) (majutsu-diff-visit-workspace-file)
Visit the workspace file without conflicting with Evil’s C-j section navigation.
Key: - (Vanilla) (majutsu-diff-less-context)
Decrease the context for diff hunks by COUNT lines.
Key: = (Evil) (majutsu-diff-less-context)
Decrease the context for diff hunks by COUNT lines.
Key: + (majutsu-diff-more-context)
Increase the context for diff hunks by COUNT lines.
Key: 0 (Vanilla) (majutsu-diff-default-context)
Reset the context for diff hunks to the default height.
Key: ~ (Evil) (majutsu-diff-default-context)
Reset the context for diff hunks to the default height.
Key: t (Vanilla) (majutsu-diff-toggle-refine-hunk)
Toggle word-level refinement.
Key: T (Vanilla) (majutsu-diff-toggle-fontify-hunk)
Toggle syntax highlighting within diff hunks. With a prefix argument, cycle between highlighting the current hunk on selection and highlighting all hunks immediately.

When Evil mode is active, Majutsu follows evil-collection’s conventions for Magit diff buffers: + still increases context, = decreases it, and ~ resets it to the default.

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).

With Vanilla bindings, file visits through RET or C-j still respect the side at point by using stored line/column metadata from the color-words backend.

With Evil bindings, RET visits the side at point and C-<return> forces the workspace file; C-j remains section navigation. File visits use the same stored line/column metadata from the color-words backend.

File and Blob Inspection

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.

Commands:

Command: majutsu-find-file
Prompt for revset + path (defaulting from point when possible) and open that file as a blob.
Command: majutsu-find-file-other-window
Like majutsu-find-file, but display the blob in another window.
Command: 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:

Key: p / n (majutsu-blob-previous / majutsu-blob-next)
Jump to the previous or next revision that touched this file, while preserving cursor position as much as possible.
Key: V (Vanilla) (majutsu-blob-visit-file)
Jump to the workspace version of the same file.
Key: C-c m (majutsu-blob-visit-magit)
Open the current blob in Magit’s blob viewer.
Key: b (majutsu-annotate-addition)
Start annotate for the current blob.
Key: g (Vanilla) (revert-buffer)
Revert or reload the current blob content.
Key: g r (Evil) (revert-buffer)
Revert or reload the current blob content.
Key: e / C-x C-q (majutsu-blob-edit-start)
Enter editable blob mode (wdired-style).

Blob navigation keys are disabled while editing, so blob-mode bindings do not interfere with text edits.

Key: C-c C-c (majutsu-blob-edit-finish)
Apply changes through non-interactive jj diffedit and exit edit mode. Majutsu copies current buffer text into the diffedit right side and finishes automatically, without opening the right-side temp file buffer.
Key: C-x C-q (majutsu-blob-edit-exit)
Leave editable mode. If modified, it prompts to save or abort; if unchanged, it exits immediately.
Key: C-c C-k (majutsu-blob-edit-abort)
Abort blob edits, restore the original content, and restore the original cursor position.

Evil normal-state behavior depends on whether the blob is read-only or editable:

Key: i (majutsu-evil-blob-insert-dwim)
Enter editable blob mode and stay in normal state.
Key: i (evil-insert-state)
Enter insert state.

Editable mode also changes cursor visuals (see majutsu-blob-edit-cursor-type); with Evil, the normal-state cursor is updated too.

Evil bindings in blob buffers keep Evil’s motions (p, n, b, e, w…) untouched; blob commands live on the g prefix and C-j / C-k, mirroring the annotate-mode keys:

Key: gj / C-j (majutsu-blob-next)
Next revision touching the file.
Key: gk / C-k (majutsu-blob-previous)
Previous revision touching the file.
Key: gb (majutsu-annotate-addition)
Start annotate for the blob.
Key: gf (majutsu-blob-visit-file)
Visit the workspace version of the file.
Key: gr
Revert or reload the blob content.
Key: q
Bury the blob buffer.
Key: RET (majutsu-edit-changeset)
Edit the blob’s revision.

Annotate (Blame)

Majutsu annotate is implemented by majutsu-annotate and uses jj file annotate under the hood. The navigation bindings below belong to majutsu-annotate-read-only-mode, which is enabled by default through majutsu-annotate-read-only. Setting that option to nil keeps ordinary file buffers editable with their editing bindings; revision blob buffers remain read-only snapshots.

Key: b (Vanilla) (majutsu-annotate-addition)
Enter annotate from a blob buffer. From an ordinary file buffer, use M-x majutsu-annotate-addition. When invoked again on an annotated chunk, jump to its parent revision (when available) and re-annotate there.
Key: c (majutsu-annotate-cycle-style)
Cycle heading/highlight/line styles (see majutsu-annotate-styles).
Key: n / p (Vanilla) (majutsu-annotate-next-chunk / majutsu-annotate-previous-chunk)
Move to the next or previous chunk.
Key: C-j / g j / C-k / g k (Evil) (majutsu-annotate-next-chunk / majutsu-annotate-previous-chunk)
Move to the next or previous chunk.
Key: N / P (Vanilla) (majutsu-annotate-next-chunk-same-commit / majutsu-annotate-previous-chunk-same-commit)
Move to the next or previous chunk from the same commit.
Key: g J / g K (Evil) (majutsu-annotate-next-chunk-same-commit / majutsu-annotate-previous-chunk-same-commit)
Move to the next or previous chunk from the same commit.
Key: RET (Vanilla) (majutsu-annotate-show-commit)
Show the chunk’s revision diff.
Key: M-w (majutsu-annotate-copy-hash)
Copy the chunk change-id.
Key: SPC (Vanilla) (majutsu-annotate-show-or-scroll-up)
Scroll the diff window up if it is already open.
Key: S-SPC (majutsu-annotate-show-or-scroll-down)
Scroll the diff window down if it is already open.

Ediff and Diffedit

Key: ? E (Vanilla) (majutsu-ediff)
Open the Ediff transient.
Key: E (Evil) (majutsu-ediff)
Open the Ediff transient.

Selection: choose revisions via --revisions or --from / --to (with point-toggle variants). In diff buffers, current range is used as default.

Key: e (majutsu-ediff-dwim)
Compare based on context (hunk, file, commit, or whole buffer).
Key: E (majutsu-ediff-edit)
Run jj diffedit with Emacs as ui.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 to jj diffedit. When majutsu-diffedit-finish-on-save is non-nil, saving a diffedit temp file can finish the with-editor session automatically.
Key: m (majutsu-ediff-resolve)
Resolve conflicted files. In a jj-commit section, it lists conflicted files for that revision; otherwise it uses working copy @. Uses jj resolve with 3-way Ediff (merge tool: $left, $base, $right) for up-to-2-sided conflicts, and falls back to jj diffedit for 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. Conflict-file completion preserves exact repository paths and annotates each candidate with its conflict side count.
Key: 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.

Resolve entry points are available both from the Ediff transient and as direct commands:

Command: majutsu-ediff-resolve
Resolve conflicted files using the default Ediff-based flow.
Command: majutsu-ediff-resolve-with-conflict
Open the conflict target directly and enable majutsu-conflict-mode.

Process Buffer

Key: $ (Vanilla) (majutsu-process-buffer)
Show the current repository’s process buffer. It retains recent synchronous and asynchronous jj commands, including failures and long-running remote operations.
Key: ` (Evil) (majutsu-process-buffer)
Show the current repository’s process buffer. It retains recent synchronous and asynchronous jj commands, including failures and long-running remote operations.

Operations

Key: X (majutsu-op-transient)
Open the operation dispatcher with log, diff, restore, revert, undo, and redo actions.
Key: v (Vanilla) (majutsu-evolog)
Open the evolution log for the revision at point.
Key: ? v (Evil) (majutsu-evolog)
Open the evolution log from the Majutsu dispatcher. Contextual v remains available in operation diff buffers.
Command: majutsu-op-log-transient
Open the operation-log menu. Supports --limit, --reversed, and --no-graph.
Command: majutsu-op-log
View Jujutsu’s operation history with its graph preserved by default. Rows mirror jj’s compact operation format: normal and snapshot operations show short id, user, workspace, elapsed time, description, and all attributes; the root shows only its short id and root(). Full ids and canonical metadata remain available to actions and the shared ? w copy submenu. Read-only operation views pass --at-op=@ --ignore-working-copy to avoid snapshotting while browsing. Press d to open an operation-diff transient prefilled from point, u to restore, or r to revert; RET keeps the ordinary Majutsu visit behavior.
Command: majutsu-op-diff-transient / majutsu-op-diff
Compare repository changes by operation, --from, or --to in a sectionized changed commit/ref view. Explicit --from / --to ranges show lightweight metadata for both endpoint operations, including their full ids, user, workspace, end time, and description. Changed lines keep full commit/change IDs internally. Press RET or v on a changed line to open its evolution log; elsewhere RET keeps ordinary Majutsu visit behavior, and d remains the ordinary Majutsu diff command. Operation metadata plus this diff view replace a separate operation-show buffer.
Command: majutsu-evolog
View jj evolog with its graph preserved and each entry parsed through Majutsu’s structured row protocol. It is also available as ? v in the dispatcher. Entries mirror jj’s compact evolog format; full change, commit, and optional operation IDs are hidden metadata available through ? w. Press RET to open jj’s native Git-format inter-diff for the entry (using jj evolog --patch --git); d remains the ordinary Majutsu diff command.
Commands: majutsu-op-restore / majutsu-op-revert
Restore to an operation or revert an individual operation after confirmation.

Manipulating

Creating Changes

Key: o (majutsu-new)
Open the New transient to specify options.

Direct log-buffer shortcuts:

Key: O (majutsu-new-dwim)
Create a new change on top of the revision at point (DWIM).
Key: A (majutsu-new-with-after)
Create a new change after the revision at point.
Key: B (Vanilla) (majutsu-new-with-before)
Create a new change before the revision at point.
Key: I (Evil) (majutsu-new-with-before)
Create a new change before the revision at point.

Useful transient entries include:

Key: -r
Parent revisions.
Key: -A
Insert after constraint.
Key: -B
Insert before constraint.
Key: -m
Set a message.
Key: -e
No edit (don’t switch to the new change).

Runs: jj new -r REV

Describing Changes

Key: c (majutsu-describe)
Edit the description of the revision at point. Supports --ignore-immutable for immutable revisions.

Runs: jj describe -r REV

Committing

Key: C (majutsu-commit)
Run jj commit through with-editor. This edits the working-copy description, finalizes that change, and lets jj create the next empty working-copy change on top.

Runs: jj commit

Editing Changes

Key: e (majutsu-edit-changeset)
Move the working copy (@) to the revision at point. Supports --ignore-immutable for immutable revisions.

Runs: jj edit REV

Squashing

Key: s (majutsu-squash)
Open the Squash transient. You can select multiple source revisions (--from) to squash into a destination (--into).

Transient arguments:

Key: -f
Source revisions (--from).
Key: -t
Destination revision (--into).
Key: -o
Onto destination.
Key: -A / -B
Insert after or before constraints.
Key: –
Limit squash to specific filesets.
Key: -k
Keep commits that become empty after squashing.
Key: -u
Use the destination revision’s description and discard source descriptions (--use-destination-message).

Runs: jj squash --from SRC --into DEST [FILESETS...]

Absorbing

Key: a (majutsu-absorb)
Open the Absorb transient.

Transient arguments:

Key: -f
Source revision (--from, default @).
Key: -t
Destination revset filters (--into, repeatable, default mutable()).
Key: –
Limit absorb to specific filesets.

Runs: jj absorb --from REV --into REVSETS... [FILESETS...]

Rebasing

Key: r (majutsu-rebase)
Open the Rebase transient. This is one of Majutsu’s most powerful features, allowing you to visually select sources and destinations.

Transient arguments:

Key: -s
Rebase a revision and its descendants (--source).
Key: -b
Rebase a whole branch relative to the destination (--branch).
Key: -r
Rebase only the selected revisions while preserving dependencies among them (--revision).
Key: -o
Rebase onto destination parent(s) (--onto).
Key: -A / -B
Insert after or before the selected destination revisions.
Key: -p
Remove redundant parent edges while rebasing (--simplify-parents).

Exactly one source style (-s, -b, or -r) and one destination style (-o, -A, or -B) may be active. The destination is required before Majutsu executes the command. Runs: jj rebase --source SRC --onto DEST

Duplicating

Key: ? y (majutsu-duplicate)
Open the Duplicate transient. Allows selecting source revisions and destination.

Transient arguments:

Key: -r
Source revisions to duplicate.
Key: -o
Destination (--onto).
Key: -A / -B
Insert after or before constraints.

Runs: jj duplicate -r REV

Abandoning

Key: k (Vanilla) (majutsu-abandon)
Remove the revision at point from visible history and rebase its descendants onto its parent(s). With an active region selection, Majutsu abandons all selected revisions. The operation remains recoverable through jj’s operation history until that history is pruned.
Key: x (Evil) (majutsu-abandon)
Remove the revision at point from visible history and rebase its descendants onto its parent(s). With an active region selection, Majutsu abandons all selected revisions. The operation remains recoverable through jj’s operation history until that history is pruned.

Runs: jj abandon REV

Undo and Redo

Key: C-/ / C-? (Vanilla) (majutsu-undo / majutsu-redo)
Undo or redo.
Key: u / C-r (Evil) (majutsu-undo / majutsu-redo)
Undo or redo.

Runs: jj undo or jj redo

Splitting

Key: S (majutsu-split)
Open the Split transient. This allows you to split a revision into multiple parts.

Transient arguments:

Key: -r
Specify the revision to split.
Key: -o
Specify the destination (--onto).
Key: -A / -B
Insert after or before constraints.
Key: -m
Set a message for the first part.
Key: -p
Parallel split mode.
Key: –
Limit split to selected files or filesets.

Runs: jj split -r REV [FILESETS...]

Restoring

Key: R (majutsu-restore)
Open the Restore transient for undoing changes.

Transient arguments:

Key: -f
Restore from a specific revision (--from).
Key: -t
Restore to a specific revision (--to).
Key: -c
Undo changes introduced by a revision (--changes-in).
Key: -d
Restore descendants as well.
Key: –
Limit restore to selected files or filesets.

Runs: jj restore --from REV [FILESETS...] or jj restore --changes-in REV [FILESETS...]

Reverting

Key: V (Vanilla) (majutsu-revert)
Open the Revert transient to apply reverse changes in one or more new revisions.
Key: _ (Evil) (majutsu-revert)
Open the Revert transient to apply reverse changes in one or more new revisions.

Transient arguments:

Key: -r
Source revisions to revert (--revision, repeatable).
Key: -o
Apply reverse changes on top of revisions (--onto).
Key: -A / -B
Insert reverse changes after or before selected revisions.

Runs: jj revert --revision REV --onto DEST

Sparse Working Copy

Key: > (majutsu-sparse)
Open the sparse working copy transient (set, add, remove, list, edit).

Sparse transient actions:

Key: l (majutsu-sparse-list)
List current sparse patterns in *Majutsu Sparse*.
Key: s / S (majutsu-sparse-set / majutsu-sparse-set-clear)
Set patterns by appending, or replace the entire set after clearing it first.
Key: a / r (majutsu-sparse-add / majutsu-sparse-remove)
Add or remove patterns incrementally.
Key: e (majutsu-sparse-edit)
Edit patterns via jj sparse edit.
Key: R (majutsu-sparse-reset)
Reset to all files (jj sparse reset).

Notes:

  • Editing patterns opens a temporary .jjsparse buffer.
  • 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 ..

Interactive Patching

Majutsu provides transient-scoped partial patch 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. The selection overlays belong to the current transient session and are cleared when that session ends; they are not a persistent staging area.

How It Works

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:

KeyCommandDescription
Hmajutsu-interactive-toggle-hunkToggle selection of hunk at point
Fmajutsu-interactive-toggle-fileToggle all file hunks or a whole-file change with no text hunks
Rmajutsu-interactive-toggle-regionToggle selection of active region
Cmajutsu-interactive-clearClear all patch selections

The -i argument is a separate, jj-native workflow. When no H/F/R patch selection is active, Majutsu forwards --interactive (and Split/Squash’s --tool argument) to jj; --interactive therefore uses the user’s configured ui.diff-editor. Majutsu does not silently replace that editor with Ediff. The H/F/R workflow instead supplies Majutsu’s own temporary patch tool when the operation is executed.

Diff Context Inheritance

When opening Split, Squash, or Restore from a Diff buffer, each transient maps the compatible part of the diff context to its own command:

  • Split converts a single --revisions source to --revision.
  • Squash converts --revisions sources to repeatable --from arguments.
  • Restore converts --revisions to --changes-in and also inherits explicit --from / --to ranges.

Use a revision diff for Split or Squash. An arbitrary --from / --to range is meaningful as inherited context only for Restore.

This means you can:

  1. View a diff with specific --from / --to revisions
  2. Open the Restore transient
  3. Select specific hunks to restore
  4. The restore will apply only to those hunks, using the diff’s revision context

Visual Feedback

Selected hunks are highlighted with the majutsu-interactive-selected-hunk face (green background by default). Selected regions within hunks use the majutsu-interactive-selected-region face (purple background). Selected file changes with no text hunks use the majutsu-interactive-selected-file face.

Selection Semantics

The meaning of “selected” differs by operation:

Split and Squash

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 move into the chosen destination; by default Majutsu infers the source revision’s external parent. Unselected content remains in the source 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:

  1. Open the diff for the revision (D)
  2. Open Squash transient (s)
  3. Press F on file A to select all its hunks
  4. Execute squash - file A’s changes go to parent, B and C stay

Restore

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

Technical Implementation

Majutsu uses a custom merge tool to apply partial patches. When you execute an operation with selections:

  1. Patch Generation: Majutsu generates a unified diff patch containing only the selected hunks/regions.

    For Restore, it instead builds complementary replay data: the unselected hunks and whole-file changes. Hunkless whole-file changes are represented by structured jj metadata rather than inferred from rendered Git headers.

  2. Tool Invocation: Jujutsu’s -i --tool mechanism is used with a custom majutsu-applypatch tool.

    Each operation stores its patch file and helper script in a separate temporary directory, so overlapping asynchronous operations do not share input files.

  3. Patch Application:
    • For Split/Squash: The tool resets $right (current state) to $left (parent state), then applies the patch forward. This results in $right containing only the selected changes.
      • For Restore: $right starts at the source state. The tool applies the complementary text patch and whole-file changes forward, reconstructing the result with the selected changes restored to their source state.

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 -.

Edge Cases

Text-hunk selection supports the file operations below. Hunkless whole-file selection is available for all three operations as described at the end of this section.

New Files

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

For Restore, selected lines are restored to the source state and unselected lines are retained. This also covers a selected portion of a newly added file without reverse-applying a new-file patch.

Deleted Files

Note: Partial text selection for deleted files is currently limited.

Select the entire deletion or none. This limitation is independent of hunkless whole-file selection, which handles file changes that have no text hunks.

Renamed/Copied Files

Renamed and copied files are handled correctly:

  • Text-hunk patches preserve rename/copy information
  • You can select specific hunks within renamed files just like regular modifications

Changes Without Text Hunks

In a Git-format diff buffer, Split, Squash, and Restore can select a file change with no text hunks, such as a binary or mode-only change, as a unit. Majutsu uses structured metadata from jj as the source of the file operation and paths, and verifies it against the displayed file headers. If the selection cannot be verified, Majutsu refuses it. Restore replays the complementary whole-file changes, so the selected file change is restored as a unit.

Workflow Example

  1. Open a diff with D or d
  2. Open Split (S), Squash (s), or Restore (R) transient
  3. Use H to select individual hunks, or F to select all hunks in a file
  4. For fine-grained control, mark a region and press R to select only those lines
  5. Press C to clear selections if needed
  6. Execute the operation - only selected changes will be affected

Customization

Face: majutsu-interactive-selected-hunk
Face for selected hunks.
Face: majutsu-interactive-selected-region
Face for selected regions.
Face: majutsu-interactive-selected-file
Face for selected file changes with no text hunks.

Metaediting

Key: ? m (majutsu-metaedit)
Open the Metaedit transient. Revisions in the active region are selected initially, followed by the revision at point, then @ as fallbacks.

Transient arguments:

Key: r / -r / c
Toggle revisions at point or in the active region, read a revset, or clear the visual revision selections. Multiple revisions are supported.
Key: -m / -a / -t
Set message, author, and author timestamp.
Key: -c / -u / -U / -f
Toggle update-change-id, update-author, update-author-timestamp, or force-rewrite.
Key: -I
Ignore immutable revisions.

Runs: jj metaedit [OPTIONS...] [REVSETS]...

Signing and Unsigning

Key: ? j (majutsu-sign)
Open the Sign transient. Revisions in the active region are selected initially, followed by the revision at point, then @ as fallbacks.
Key: ? J (majutsu-unsign)
Open the Unsign transient with the same visual revision selection.

Transient arguments:

Key: r / -r / c
Toggle revisions at point or in the active region, read a revset, or clear the visual revision selections.
Key: -k
Override the configured signing key for jj sign. Majutsu completes secret signing keys for the gpg and gpgsm backends, and the configured key plus ~/.ssh/*.pub for the ssh backend. Arbitrary values remain available for backend-specific key formats. A commit-signing backend must be configured in jj.
Key: -I
Ignore immutable revisions.

Runs: jj sign --revision REVSETS... or jj unsign --revision REVSETS...

Simplifying Parents

Key: ? P (majutsu-simplify-parents)
Open the Simplify Parents transient.

Transient arguments:

Key: -s
Simplify specified revision(s) together with descendant trees (--source).
Key: -r
Simplify specified revision(s) only (--revision).
Key: -I
Ignore immutable revisions.

If neither --source nor --revision is set, the Simplify action defaults to selected region revisions, then the revision at point, then @. Runs: jj simplify-parents [--source REVSET] [--revision REVSET]

Conflict Resolution

Detecting Conflicts

JJ marks conflicts in the log with a “conflict” label. Expanding the revision shows each exact conflicted path together with its conflict side count.

Entering Conflict Mode

You can enter conflict handling from multiple places:

Key: m in the Ediff transient (majutsu-ediff-resolve)
Use the automatic resolve strategy.
Key: M in the Ediff transient (majutsu-ediff-resolve-with-conflict)
Force the conflict workflow. This opens the working-copy file (or a revision blob buffer for non-working-copy revisions), enables majutsu-conflict-mode, and jumps to the first conflict.
Command: majutsu-conflict-ensure-mode
Enable conflict handling in a file with conflict markers.

majutsu-conflict auto-detects marker styles:

  • JJ conflict markers (%%%%%%% / +++++++ / ------- ) -> enable majutsu-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 -> enable smerge-mode.

majutsu-conflict-mode Commands

Vanilla bindings use the prefix C-c ^:

Key: n / p (majutsu-conflict-next / majutsu-conflict-prev)
Jump to the next or previous conflict.
Key: b (majutsu-conflict-keep-base)
Keep the conflict base (snapshot / rebase destination section).
Key: 1..9 (majutsu-conflict-keep-side)
Keep side N (the “after” side in JJ diff blocks).
Key: M-1..M-9 (majutsu-conflict-keep-side)
Keep side N’s “before” variant.
Key: R (majutsu-conflict-refine)
Refine conflict regions with word-level highlighting.

Evil bindings are available in Normal state:

Key: ]] / gj (majutsu-conflict-next)
Jump to the next conflict.
Key: [[ / gk (majutsu-conflict-prev)
Jump to the previous conflict.
Key: gb (majutsu-conflict-keep-base)
Keep the base.
Key: gr 1..9 (majutsu-conflict-keep-side)
Keep side N (the “after” side).
Key: gR 1..9 (majutsu-conflict-keep-side)
Keep side N’s “before” variant.
Key: ge (majutsu-conflict-refine)
Refine the conflict.

Bookmarks

Understanding Bookmarks

In Jujutsu, bookmarks are named references similar to Git branches. A local bookmark normally targets one revision and may explicitly track a remote bookmark; concurrent moves can also leave a bookmark with conflicted targets.

Bookmark List

The bookmark list always includes local and remote bookmarks, including synchronized and untracked remote bookmarks. No prefix argument is needed.

Local bookmarks and Remote bookmarks are separate sections; remote bookmarks are grouped by remote name, with fetch URLs and distinct push URLs shown in the remote headings. Configured remotes without bookmarks show No bookmarks. On a remote heading, r opens remote management, f fetches only that remote, and the delete action removes the remote. Both sections remain visible when empty. Git’s special @git references appear separately under Git tracking bookmarks when present.

Remote rows show their tracking state. For tracked bookmarks, synced means there are no commits ahead or behind the local bookmark; +N/-N counts are relative to the local bookmark, from the remote’s perspective. Deleted local bookmarks remain visible, and their tracked remote rows say local deleted. Conflict targets appear beneath their own bookmark. Sections initially open and retain their folding state on refresh.

Embark Actions

When Embark is loaded, embark-act recognizes bookmark list sections and completion candidates. No additional Majutsu key binding is installed; use your configured Embark key in either Emacs or Evil.

Local bookmarks offer inspect, edit, move, rename, delete and forget actions. Remote bookmarks offer inspection and exactly scoped tracking actions: t tracks an untracked reference and u untracks a tracked reference. The special @git records offer revision inspection without local bookmark mutation actions. Conflict children target their own commit, not their parent bookmark. Remote headings offer fetch, rename and remove, including remotes with no bookmarks. Copying remains available through Embark’s standard actions.

The default bookmark/revision action shows its diff; e explicitly edits the working copy. Commands such as move may still prompt for their remaining arguments. The ordinary bookmark, fetch and push transients retain their parameter controls and remain available without Embark.

Bookmark Transient

Key: b (majutsu-bookmark)
Open the bookmark transient.

Transient actions:

Key: l (majutsu-bookmark-list)
List all bookmarks.
Key: c (majutsu-bookmark-create)
Create a new bookmark at point.
Key: a (majutsu-bookmark-advance)
Advance bookmarks using jj’s configured default selection and target revset.
Key: A (majutsu-bookmark-advance-to)
Advance bookmarks using jj’s default selection to an explicit target revset.
Key: p (majutsu-bookmark-advance-patterns)
Advance specific bookmark name(s)/pattern(s) using jj’s configured default target revset.
Key: s (majutsu-bookmark-set)
Set or move an existing bookmark to the current revision.
Key: m (majutsu-bookmark-move)
Move a bookmark.
Key: M (majutsu-bookmark-move-allow-backwards)
Move bookmark(s) with --allow-backwards.
Key: r (majutsu-bookmark-rename)
Rename a bookmark.
Key: d (majutsu-bookmark-delete)
Delete a bookmark locally and record the deletion for propagation on a later push; this command does not contact a remote itself.
Key: f (majutsu-bookmark-forget)
Forget a bookmark locally without affecting remotes.
Key: t (majutsu-bookmark-track)
Track a remote bookmark to create a local one.
Key: u (majutsu-bookmark-untrack)
Untrack a remote bookmark.

Bookmark lists nest tracked remote bookmarks under their local bookmark. When a bookmark target is conflicted, its removed and added targets appear as child revision sections identified by their full commit IDs.

Tags

Understanding Tags

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 Transient

Tag commands are intentionally dispatch-first.

Key: ? t (majutsu-tag)
Open the tag transient.

Transient actions:

Key: l (majutsu-tag-list)
List tags in a dedicated buffer.
Key: s (majutsu-tag-set)
Set tag(s). Uses completion and allows new names.
Key: m (majutsu-tag-move)
Move existing tag(s) to another revision (internally --allow-move).
Key: d (majutsu-tag-delete)
Delete tag(s) by name or 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.

Git Integration

Git Transient

Key: G (Vanilla) (majutsu-git-transient)
Open the Git transient. Jujutsu can interact directly with Git remotes.
Key: ? G (Evil) (majutsu-git-transient)
Open the Git transient. Jujutsu can interact directly with Git remotes.

Pushing

Key: p (majutsu-git-push-transient)
Open the Push transient. You can push bookmarks, tags, revisions, or changes.

Transient arguments:

Key: -a
Push all bookmarks and tags.
Key: -t
Push tracked bookmarks and tags only.
Key: -b
Push specific bookmark(s).
Key: -T
Push specific tag(s) or tag patterns.
Key: -c
Push a specific change.
Key: -r
Push specific revision(s).
Key: -o
Pass a Git push option; may be repeated.
Key: -y
Show what would be pushed without pushing (--dry-run).
Key: W
Save stable push options as defaults for this jj repository. Majutsu remembers sync policy such as --remote, --all, --tracked, --deleted, --allow-empty-description, and --allow-private, but does not remember one-off targets like --bookmark, --tag, --change, --revision, --named, --option, or --dry-run.

Runs: jj git push

Fetching

Key: f (majutsu-git-fetch-transient)
Open the Fetch transient to pull changes from a remote.

Transient arguments:

Key: -R
Fetch from a specific remote; may be repeated.
Key: -b
Fetch a specific branch; may be repeated.
Key: -T
Fetch specific tag(s) or tag patterns; may be repeated.
Key: -t
Fetch only tracked bookmarks and tags.
Key: -A
Fetch from all remotes.
Key: W
Save stable fetch options as defaults for this jj repository. Majutsu remembers --remote, --tracked, and --all-remotes, but not one-off --branch or --tag selections.

Runs: jj git fetch

Gerrit Upload

Key: u (majutsu-gerrit-upload-transient)
Open the Gerrit upload transient.

Transient arguments and selection controls:

Key: -r
Upload specific revision(s).
Key: r
Toggle the revision at point into or out of the upload selection. If no revision is selected, Majutsu leaves revision selection to jj gerrit upload, which uploads @ when it has a description and @- otherwise.
Key: -b
Target remote branch (--remote-branch).
Key: -R
Gerrit remote (--remote).
Key: -n
Preview without pushing (--dry-run).
Key: W
Save stable upload options as defaults for this jj repository. Majutsu remembers --remote, --remote-branch, --notify, and --ignore-attention-set, but not revisions, dry-run, metadata, state, or submit-related flags.

The remaining groups expose:

  • Review options include reviewers, CCs, labels, topic, hashtags, and a patch set message.
  • State/notification options include change edit, WIP/ready, private/remove-private, publishing comments, notification policy, and attention-set handling.
  • Advanced options expose submit, skip validation, merged uploads, deadlines, custom keyed values, and trace values.

Runs: jj gerrit upload

Reviewer/CC prompts use Majutsu’s native Gerrit REST client and Gerrit’s /accounts/ suggestion endpoint. Topic completion uses the same REST client to query open changes and aggregate existing topics. Label and hashtag prompts are free-form minibuffer input.

REST-backed completion is enabled only when Majutsu can determine a reliable Gerrit web/API origin. HTTP(S) remotes work directly. For SSH remotes such as ssh://user@host:29418/project, configure Gerrit’s web URL explicitly, for example:

jj config set --repo gerrit.review-url http://host:8080

Majutsu intentionally does not guess a web/API URL from SSH port 29418. This setting is not required for jj gerrit upload itself; upstream jj uses gerrit.review-url only to generate Link: trailers instead of Change-Id: trailers. Majutsu also uses it as a reliable web/API base for native Gerrit completion.

Remotes

Key: r (majutsu-git-remote-transient)
Open the Remote Management transient.

Transient actions:

Key: l (majutsu-git-remote-list)
Open the bookmark list at its Remote bookmarks section. Configured remotes with no bookmarks are included.
Key: a (majutsu-git-remote-add-transient)
Add a new Git remote as jj git remote add [OPTIONS] REMOTE URL. Majutsu prompts for a new remote name, rejects names that already exist, prompts for the URL/path positional, and exposes --fetch-tags and --push-url.
Key: d (majutsu-git-remote-remove)
Remove an existing remote. In the bookmark list buffer, the remote at point is used as the default.
Key: r (majutsu-git-remote-rename)
Rename a remote.
Key: u (majutsu-git-remote-set-url-transient)
Set remote URLs. With no option, Majutsu prompts for the fetch URL/path positional; use --fetch and/or --push to set them explicitly. In the bookmark list buffer, the remote at point is used as the default.

Clone and Init

Key: c (majutsu-git-clone-transient)
Clone a Git repository into a jj repo. Source may be a URL or path; --branch can be repeated.
Key: i (majutsu-git-init-transient)
Initialize a new Git-backed jj repository.

Export and Import

In a colocated Git-backed workspace, Jujutsu automatically imports from and exports to the underlying Git repository on each command. In other Git-backed layouts, use the commands below to synchronize the repositories explicitly:

Key: e (majutsu-git-export)
Run jj git export.
Key: m (majutsu-git-import)
Run jj git import.
Key: o (majutsu-git-root)
Show the Git directory path (jj git root).

Workspaces

Understanding Workspaces

Jujutsu supports multiple workspaces sharing the same repository storage. This is similar to Git worktrees but more integrated.

Embark Actions

When Embark is loaded, workspace completion candidates offer RET / v to visit the workspace, d to open its root in Dired, and W to copy its root. Actions run in the repository directory associated with the completion.

Workspace Transient

Key: Z / % (Vanilla) (majutsu-workspace)
Open the workspace transient.
Key: * (Evil) (majutsu-workspace)
Open the workspace transient.

Transient actions:

Key: l (majutsu-workspace-list)
List all workspaces.
Key: v (majutsu-workspace-visit)
Visit a workspace by changing default-directory and refreshing Majutsu buffers.
Key: a (majutsu-workspace-add)
Add a new workspace.

Forget option:

Key: -t
Toggle trashing workspace directories after forgetting them.

More transient actions:

Key: f (majutsu-workspace-forget)
Forget a workspace. With -t enabled, also move its local directory to the system trash after jj workspace forget succeeds; this trash step accepts local roots only, while plain forget can still forget a remotely located workspace. Before moving a directory, Majutsu verifies that the path still identifies the directory the user confirmed and does not own the shared .jj/repo store. It checks all selected workspaces and reports all paths requiring manual cleanup together.
Key: u (majutsu-workspace-update-stale)
Update a stale workspace.
Key: n (majutsu-workspace-rename)
Rename a workspace.
Key: r (majutsu-workspace-root)
Show and copy the 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. Workspace paths containing embedded newlines or other control characters are preserved as complete values.

Customizing

Essential Settings

User Option: majutsu-jj-executable
Path to the jj binary (default: "jj").
User Option: majutsu-jj-global-arguments
Global arguments passed to all jj commands (default: ("--no-pager" "--color=always")).
User Option: majutsu-jj-diffstat-columns
Column width used only for jj diff --stat (default: 80). Set to nil to inherit terminal width. Raise it if you want more full path visibility; keep it near 80 if you prefer compact stat bars.
User Option: majutsu-transient-default-action
Key used for primary/default action suffixes in Majutsu transients (default: RET). Set it to another key description, such as ., to move the default action key.
User Option: majutsu-show-process-buffer-hint
Whether to append a process-buffer hint to jj errors. The displayed key is resolved from the active bindings, so Vanilla normally shows $ and Evil normally shows `.
User Option: majutsu-debug
Enable debug logging for jj operations.
User Option: majutsu-show-command-output
Show jj command output in messages.

Confirmation Settings

User Option: majutsu-confirm-critical-actions
If non-nil, prompt for confirmation before critical operations like abandon, undo, redo, rebase.
User Option: majutsu-no-confirm
A list of symbols for actions Majutsu should not confirm, or t to never confirm. Valid symbols: undo, redo, abandon, bookmark-delete, tag-delete, git-remote-remove, rebase, workspace-forget, and workspace-trash.
User Option: majutsu-slow-confirm
A list from the same action set that should use yes-or-no-p instead of y-or-n-p, or t for all of them.

Process Options

User Option: majutsu-process-popup-time
Popup the process buffer if a command takes longer than this many seconds. -1 means never, 0 means immediately.
User Option: majutsu-process-log-max
Maximum number of sections to keep in a process log buffer (default: 32).
User Option: majutsu-process-apply-ansi-colors
When non-nil, convert ANSI escapes in jj output to text properties.
User Option: majutsu-process-timestamp-format
Format string for process log section timestamps.
User Option: majutsu-jj-environment
Extra KEY=VALUE entries prepended to subprocess environments for all jj invocations (local and TRAMP), applied centrally by majutsu-process.

Display Options

User Option: majutsu-log-commit-columns
Flat log row column declarations controlling templates, modules, face policy, and postprocessing. For protocol details, module semantics, and examples, see Inspecting -> Log Buffer.
Template family: majutsu-log-template-<field>
Template forms consumed by log fields. For newline and control-character rules, see Inspecting -> Log Buffer.
User Option: majutsu-log-sections-hook
Hook run to insert sections in the log buffer.
User Option: majutsu-display-buffer-function
Magit-style display strategy (default: majutsu-display-buffer-traditional). Built-in alternatives are majutsu-display-buffer-same-window-except-diff-v1, majutsu-display-buffer-fullframe-log-v1, majutsu-display-buffer-fullframe-log-topleft-v1, majutsu-display-buffer-fullcolumn-most-v1, and display-buffer.
User Option: majutsu-pre-display-buffer-hook
Hook run before displaying a buffer.
User Option: majutsu-post-display-buffer-hook
Normal hook run after displaying a buffer.
User Option: majutsu-bury-buffer-function
Function used by q to bury/quit Majutsu buffers (default: majutsu-mode-quit-window; alternatives include quit-window and majutsu-restore-window-configuration).

Diff Options

User Option: majutsu-diff-refine-hunk
Whether to show word-granularity differences inside hunks. nil disables, t refines the current hunk, and 'all refines all hunks.
User Option: majutsu-diff-fontify-hunk
Whether to apply syntax highlighting to diff hunks. nil disables, t fontifies a hunk when it becomes current, and 'all fontifies all hunks immediately. This is synchronous and can be slow on large diffs.
User Option: majutsu-diff-refine-ignore-whitespace
Whether to ignore whitespace while refining hunks.
User Option: majutsu-diff-paint-whitespace
Whether to highlight whitespace issues inside diff hunks.
User Option: majutsu-diff-highlight-trailing
Whether to mark trailing whitespace in diff hunks.

Buffer Hooks

User Option: majutsu-create-buffer-hook
Normal hook run when a new Majutsu buffer is created.
User Option: majutsu-setup-buffer-hook
Normal hook run after displaying the buffer.
User Option: majutsu-post-create-buffer-hook
Normal hook run after the initial refresh.
User Option: majutsu-refresh-buffer-hook
Normal hook run after refreshing.

Description Options

User Option: majutsu-jjdescription-major-mode
Major mode used for editing JJ description buffers (default: text-mode).
User Option: majutsu-jjdescription-summary-max-length
Column beyond which summary line characters are highlighted (default: 68, same as Magit). Set this option to nil to disable highlighting; zero highlights the entire nonempty summary.
User Option: majutsu-jjdescription-fill-column
Local fill-column used in JJ description buffers (default: 72). A positive integer enables Auto Fill for the body but never folds the summary line; filling is delegated to the selected major mode, so constructs that mode protects (such as Org headings, tables, and blocks) remain intact. Set this option to nil to disable Auto Fill and leave the buffer’s current fill-column unchanged.
User Option: majutsu-jjdescription-comment-prefix
Comment prefix used in JJ description buffers (default: "JJ:").
Face: majutsu-jjdescription-change-id-face
Face used for JJ Change ID values.
Minor mode: global-majutsu-jjdescription-mode
Global minor mode to enable JJ description buffer enhancements.

Evil Integration

User Option: majutsu-evil-enable-integration
Set to nil to disable automatic Evil bindings.
User Option: majutsu-evil-initial-state
The Evil state to start in (default: normal).

When Evil integration is enabled, the dispatcher displays and accepts the same Evil-facing top-level keys used in Majutsu buffers, including x, L, _, *, u, C-r, and `. The global Evil v binding remains Visual state; open evolog from the dispatcher with ? v.

Template DSL

Overview

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.

Why Use the DSL?

The DSL provides several advantages over writing raw template strings:

  • Early validation: Constant vector forms with an explicit constant self type are validated during macro expansion/byte-compilation. Dynamic forms and forms that rely on the configurable default self type are validated when Majutsu compiles them, before they are sent to jj.
  • Automatic escaping: String literals are properly escaped (quotes, backslashes, control characters) without manual intervention.
  • Elisp integration: Embed Elisp expressions that are evaluated when Majutsu compiles the template—during macro expansion for constant forms and at runtime for dynamic forms—so configuration and context can shape the result.
  • Composability: Define reusable template functions with majutsu-template-defun that lower into the final template instead of requiring jj-side aliases.
  • 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)

Basic Syntax

Vectors and Concatenation

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).

String Literals

Use :str for explicit string literals with proper escaping:

(majutsu-tpl [:str "Hello \"World\""])  ; => "Hello \"World\""

Raw Injection

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\")"

Booleans and Numbers

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\")"

Method Calls and Self

Explicit Method Chaining

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\")"

Implicit Self Context

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()"

Explicit Receiver References

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())"

Binding Self in Helper Bodies

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.

Operators

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\")"

Conditionals and Composition

(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 Embedding

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 and Higher-Order Operations

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 [:method [:raw "self.parents()"]
                      :map [:lambda [p] [:raw "p.commit_id()"]]
                      :join [:str ", "]])
; => "self.parents().map(|p| p.commit_id()).join(\", \")"

Prefer direct lambdas and :map + :join composition in new code. The historical binder form [:map collection var body] remains as compatibility sugar, but it now lowers 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(), \"!\"))"

Extending the DSL

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))"

Type System and Upstream Alignment

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 Commit

Without 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.

Supported Commands (for development reference)

Templates can be used with these jj commands:

CommandSelf Type
logCommit
showCommit
evologCommitEvolutionEntry
diffTreeDiffEntry
bookmark listCommitRef
tag listCommitRef
file annotateAnnotationLine
file listTreeEntry
file showTreeEntry
op logOperation
op showOperation
workspace listWorkspaceRef
config listNamed keyword fields; value is ConfigValue

Unlike the object-oriented rows above, jj config list exposes named keywords (name, value, overridden, source, and path) rather than a single ConfigValue receiver.

Keystroke Index

Function and Command Index

Variable Index