Skip to content

Latest commit

 

History

History
1160 lines (949 loc) · 56 KB

File metadata and controls

1160 lines (949 loc) · 56 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 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.

About Jujutsu

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.

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.
  • Jujutsu (jj) v0.39.0 or later installed and in your PATH.
  • magit 3.3.0 or later (for section management).
  • transient 0.5.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 with package-vc

Emacs 29+ users can use the built-in package-vc support:

(use-package majutsu
  :vc (:url "https://github.com/0WD0/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

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.

Getting Started

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.

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

Shows the output of background jj commands. If a command fails, you can press $ to inspect the error.

JJ description Buffer

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.

Sections

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

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.

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.

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. Destructive operations like abandon or undo will prompt for confirmation.

Running JJ

Majutsu runs jj commands asynchronously whenever possible to keep the Emacs UI responsive. Large log graphs or remote operations won’t freeze your editor.

Inspecting

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. Collapsed by default, you can expand a revision to see foldable body module content (for example, long descriptions).

Log Options Transient

Press l to open the log options. Here you can:

r
Set a revset filter (e.g., all() or mine()).
-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.

Revset Builder

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.

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 Schema

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.

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.
  • Rendered log spans are tagged with majutsu-log-module, majutsu-log-field, majutsu-log-column, majutsu-log-entry-id, and majutsu-log-decoration so explicit copy commands can distinguish content from graph/tail decorations.
  • Hidden transport fields always include id, commit-id, and parent-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.

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 layout

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

Keep heading multiline + encoded body multiline

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

Custom postprocessor

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

Migration Notes

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.

Copying From Log Buffers

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, or body), 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.

Diffing

Diff Transient

Press d to open the Diff transient.

r
Select revisions to diff (--revisions). Can specify multiple revisions or a revset like B::D.
f / t
Select the --from and --to revisions.
--
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.

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:

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

Diff Buffer

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-j may be overridden by Evil’s section navigation; use C-<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.

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.

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-interactive jj diffedit and 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-q runs majutsu-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, i enters editable blob mode and stays in normal state.
    • In editable mode, i enters insert state.
  • Editable mode also changes cursor visuals (see majutsu-blob-edit-cursor-type); with Evil, normal-state cursor is updated too.

Annotate (Blame)

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 b again on an annotated chunk jumps to its parent revision (when available) and re-annotates there.
  • Styles: majutsu-annotate-cycle-style is available. In default read-only annotate bindings, press c to cycle heading/highlight/line styles (majutsu-annotate-styles).
  • Navigation: n / p for next/previous chunk, N / P for next/previous chunk from the same commit.
  • Inspection: RET shows the chunk’s revision diff, M-w copies the chunk change-id, and SPC / S-SPC scroll the diff window if it is already open.

Ediff and Diffedit

Press E in dispatcher to open majutsu-ediff transient.

  • Selection: choose revisions via --revisions or --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 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-edit-finish-on-save is 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-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.
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.

Process Buffer

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.

Operation Log (Experimental)

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.

Manipulating

Creating Changes

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

Describing Changes

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

Runs: jj describe -r REV

Committing

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

Editing Changes

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

Runs: jj edit REV

Squashing

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

Absorbing

a (Emacs/Evil), or ? then A (majutsu-absorb)
Open the Absorb transient.
-f
Source revision (--from, default @).
-t
Destination revset filter (--into, default mutable()).
--
Limit absorb to specific filesets.

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

Rebasing

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

Duplicating

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

Abandoning

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

Undo and Redo

Emacs: C-/ / C-?
Undo / Redo
Evil: u / C-r
Undo / Redo

Runs: jj undo or jj redo

Splitting

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

Restoring

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

Reverting

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

Sparse Working Copy

> (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 (with majutsu-jjsparse-mode).
R
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 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.

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 selection of all hunks in file
Rmajutsu-interactive-toggle-regionToggle selection of active region
Cmajutsu-interactive-clearClear all patch selections

Diff Context Inheritance

When opening Split, Squash, or Restore from a Diff buffer, the transient automatically inherits the diff’s context:

  • All three commands inherit --revisions as their target revision (--revision for Split/Squash)
  • Restore additionally inherits --from and --to parameters, allowing selective restoration between arbitrary revisions

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 majutsu-interactive-selected-hunk face (green background by default). Selected regions within hunks use majutsu-interactive-selected-region face (purple background).

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

  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.
  2. Tool Invocation: Jujutsu’s -i --tool mechanism is used with a custom majutsu-applypatch tool.
  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: The tool applies the patch directly to $right.

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

Interactive patching supports all file operation types:

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
  • Partial selection of new files works correctly because the patch is applied forward after resetting to parent state

Deleted Files

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/Copied Files

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

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

majutsu-interactive-selected-hunk
Face for selected hunks
majutsu-interactive-selected-region
Face for selected regions

Metaediting

? then m (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...]

Simplifying Parents

? then P (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]

Bookmarks

Understanding Bookmarks

In Jujutsu, bookmarks are similar to Git branches but are explicitly tracked. They point to a specific change ID.

Bookmark Transient

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.

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

Git Integration

Git Transient

Press G (Emacs) / ? G (Evil) to access Git-specific commands. Jujutsu can interact directly with Git remotes.

Pushing

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

Fetching

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

Remotes

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.

Clone and Init

c
Clone a Git repository into a jj repo.
i
Initialize a new Git-backed jj repository.

Export and Import

Jujutsu automatically exports/imports to the underlying Git repo, but you can trigger it manually:

e
jj git export
m
jj git import
o
Show 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.

Workspace Transient

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.

Conflict Resolution

Detecting Conflicts

JJ marks conflicts in the log with a “conflict” label. Expanding the revision will show which files are conflicted.

Entering Conflict Mode

You can enter conflict handling from multiple places:

  • In Ediff transient, use m (auto strategy) or M (force conflict workflow). M 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.
  • Manually, run M-x majutsu-conflict-ensure-mode 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

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:

]] / [[ or gj / gk
Next/previous conflict.
gb
Keep base.
gr and gR
Resolve “after” / “before” side maps.
ge
Refine.

Customizing

Essential Settings

majutsu-jj-executable
Path to the jj binary (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 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.
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.

Confirmation Settings

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 t to never confirm. Valid symbols: undo, redo, abandon, rebase, workspace-forget.
majutsu-slow-confirm
A list of actions that should use yes-or-no-p instead of y-or-n-p.

Process Options

majutsu-process-popup-time
Popup the process buffer if a command takes longer than this many seconds. -1 means never, 0 means 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=VALUE entries prepended to subprocess environments for all jj invocations (local and TRAMP), applied centrally by majutsu-process.

Display Options

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 q to bury/quit Majutsu buffers (default: majutsu-mode-quit-window; alternatives include quit-window and majutsu-restore-window-configuration).

Diff Options

majutsu-diff-refine-hunk
Whether to show word-granularity differences inside hunks. nil disables, t refines current hunk, 'all refines 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.

Buffer Hooks

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.

Description Options

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.

Evil Integration

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

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:

  • 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-defun that 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)

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 [: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(), \"!\"))"

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 listConfigValue

Keystroke Index

Function and Command Index

Variable Index