An interactive and powerful Jujutsu interface for Neovim, inspired by Magit, forked from Neogit
Lua
74
4,745 commits
updated Sep 16, 2026
Neojj is a hard fork of Neogit, adapted to work with jj (Jujutsu VCS) instead of git.
Maintainer: Nicholas Zolton
Note: Some features (specifically diffview and codediff integration) currently require a git-colocated repository. Work is underway to remove this dependency.
Requires jj (Jujutsu VCS) to be installed and available on your PATH.
Here's an example spec for Lazy, but you're free to use whichever plugin manager suits you.
{
"NicholasZolton/neojj",
version = "^1.0.0", -- recommended: track 1.x releases (see Versioning below)
lazy = true,
dependencies = {
"nvim-lua/plenary.nvim", -- required
-- Only one of these is needed.
"sindrets/diffview.nvim", -- optional
"esmuellert/codediff.nvim", -- optional
-- Only one of these is needed.
"nvim-telescope/telescope.nvim", -- optional
"ibhagwan/fzf-lua", -- optional
"nvim-mini/mini.pick", -- optional
"folke/snacks.nvim", -- optional
},
cmd = "Neojj",
keys = {
{ "<leader>gg", "<cmd>Neojj<cr>", desc = "Show Neojj UI" }
}
}
NeoJJ follows Semantic Versioning: breaking changes only
land in major releases. With version = "^1.0.0", lazy.nvim installs the
latest 1.x tag — you get new features and fixes on :Lazy update, but a
breaking 2.0 requires you to bump the pin yourself. Drop the version line
to track main directly (bleeding edge, may break without warning). Each
release is documented in CHANGELOG.md.
You can either open Neojj by using the Neojj command:
:Neojj " Open the status buffer in a new tab
:Neojj cwd=<cwd> " Use a different repository path
:Neojj cwd=%:p:h " Uses the repository of the current file
:Neojj kind=<kind> " Open specified popup directly
:Neojj commit " Open commit popup
:Neojj bookmark " Open bookmark popup
:Neojj workspace " Open workspace popup
" Map it to a key
nnoremap <leader>gg <cmd>Neojj<cr>
-- Or via lua api
vim.keymap.set("n", "<leader>gg", "<cmd>Neojj<cr>", { desc = "Open Neojj UI" })
Or using the lua api:
local neojj = require('neojj')
-- open using defaults
neojj.open()
-- open a specific popup
neojj.open({ "commit" })
-- open as a split
neojj.open({ kind = "split" })
-- open with different project
neojj.open({ cwd = "~" })
-- You can map this to a key
vim.keymap.set("n", "<leader>gg", neojj.open, { desc = "Open Neojj UI" })
-- Wrap in a function to pass additional arguments
vim.keymap.set(
"n",
"<leader>gg",
function() neojj.open({ kind = "split" }) end,
{ desc = "Open Neojj UI" }
)
The kind option can be one of the following values:
tab (default)replacesplitsplit_abovesplit_above_allsplit_belowsplit_below_allvsplitfloatingauto (vsplit if window would have 80 cols, otherwise split)If you are coming from git, here are some key differences in jj:
jj undo.You can configure Neojj by running the require('neojj').setup {} function, passing a table as the argument.
local neojj = require("neojj")
neojj.setup {
-- Path to jj binary. "auto" = auto-detect (resolves shims for performance).
jj_binary = "auto",
-- Highlight changed words inside paired deleted/added lines.
word_diff_highlight = true,
-- Hides the hints at the top of the status buffer
disable_hint = false,
-- Shows the project name at the top of the status buffer
show_project_header = true,
-- Disables changing the buffer highlights based on where the cursor is.
disable_context_highlighting = false,
-- Uses Tree-sitter parsers to apply syntax highlighting inside diff hunks.
treesitter_diff_highlight = false,
-- Highlights changed words within paired deletion and addition lines.
word_diff_highlight = true,
-- Disables signs for sections/items/hunks
disable_signs = false,
-- Changes what mode the Commit Editor starts in. `true` will leave nvim in normal mode, `false` will change nvim to
-- insert mode, and `"auto"` will change nvim to insert mode IF the commit message is empty, otherwise leaving it in
-- normal mode.
disable_insert_on_commit = "auto",
-- When enabled, will watch the `.jj/` directory for changes and refresh the status buffer in response to filesystem
-- events.
filewatcher = {
enabled = true,
},
-- "ascii" is the graph the jj CLI generates
-- "unicode" is a unicode graph style
graph_style = "ascii",
-- Show relative date by default. When set, use `strftime` to display dates
commit_date_format = nil,
log_date_format = nil,
-- Show message with spinning animation when a jj command is running.
process_spinner = false,
-- Allows a different telescope sorter. Defaults to 'fuzzy_with_index_bias'. The example below will use the native fzf
-- sorter instead. By default, this function returns `nil`.
telescope_sorter = function()
return require("telescope").extensions.fzf.native_fzf_sorter()
end,
-- Persist the values of switches/options within and across sessions
remember_settings = true,
-- Scope persisted settings on a per-project basis
use_per_project_settings = true,
-- Table of settings to never persist. Uses format "Filetype--cli-value"
ignored_settings = {},
-- Configure highlight group features
highlight = {
italic = true,
bold = true,
underline = true
},
-- Set to false if you want to be responsible for creating _ALL_ keymappings
use_default_keymaps = true,
-- Change the default way of opening Neojj
kind = "tab",
-- Floating window style
floating = {
relative = "editor",
width = 0.8,
height = 0.7,
style = "minimal",
border = "rounded",
},
-- Disable line numbers
disable_line_numbers = true,
-- Disable relative line numbers
disable_relative_line_numbers = true,
-- The time after which an output console is shown for slow running commands
console_timeout = 2000,
-- Automatically show console if a command takes more than console_timeout milliseconds
auto_show_console = true,
-- If `auto_show_console` is enabled, specify "output" (default) to show
-- the console always, or "error" to auto-show the console only on error
auto_show_console_on = "output",
-- Automatically close the console if the process exits with a 0 (success) status
auto_close_console = true,
notification_icon = "",
-- Shell command to run in new workspace directory after creation. {path} is replaced with the workspace path.
workspace_open_command = nil,
-- Shell command to initialize a new workspace before opening. {path} is replaced with the workspace path.
workspace_initialize_command = nil,
-- Base directory for quick-add worktrees (used by workspace popup's quick add action)
workspace_worktrees_directory = "~/.worktrees",
status = {
show_head_commit_hash = true,
recent_commit_count = 10,
HEAD_padding = 10,
HEAD_folded = false,
mode_padding = 3,
mode_text = {
M = "modified",
N = "new file",
A = "added",
D = "deleted",
C = "copied",
R = "renamed",
["?"] = "",
},
},
commit_editor = {
kind = "tab",
show_diff = true,
diff_split_kind = "split",
spell_check = true,
},
commit_select_view = {
kind = "tab",
},
commit_view = {
kind = "vsplit",
verify_commit = vim.fn.executable("gpg") == 1,
},
log_view = {
kind = "tab",
},
reflog_view = {
kind = "tab",
},
preview_buffer = {
kind = "floating_console",
},
popup = {
kind = "split",
},
signs = {
-- { CLOSED, OPENED }
hunk = { "", "" },
item = { ">", "v" },
section = { ">", "v" },
},
-- Each Integration is auto-detected through plugin presence, however, it can be disabled by setting to `false`
integrations = {
-- If enabled, use telescope for menu selection rather than vim.ui.select.
-- Allows multi-select and some things that vim.ui.select doesn't.
telescope = nil,
-- Neojj only provides inline diffs. If you want a more traditional way to look at diffs, you can use `diffview`.
-- The diffview integration enables the diff popup.
--
-- Requires you to have `sindrets/diffview.nvim` installed.
diffview = nil,
-- Alternative diff viewer integration.
-- Requires you to have `esmuellert/codediff.nvim` installed.
codediff = nil,
-- If enabled, uses fzf-lua for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `ibhagwan/fzf-lua` installed.
fzf_lua = nil,
-- If enabled, uses mini.pick for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `echasnovski/mini.pick` installed.
mini_pick = nil,
-- If enabled, uses snacks.picker for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `folke/snacks.nvim` installed.
snacks = nil,
},
-- Which diff viewer to use. nil = auto-detect (tries diffview first, then codediff).
-- Can be "diffview" or "codediff".
diff_viewer = nil,
-- Place new CodeDiff tabs before or after the current tab.
codediff_tab_position = "after",
-- Forge (GitHub, ...) integration. When enabled and the forge CLI (`gh`) is
-- installed, bookmarks with a matching open PR are annotated with the PR
-- number, and "o" on such a line opens the PR instead of the commit.
forge = {
pr_integration = true,
-- Extra remote hosts per provider, e.g. for GitHub Enterprise:
-- hosts = { github = { "git.yourcompany.com" } },
hosts = {},
},
sections = {
files = {
folded = false,
hidden = false,
},
conflicts = {
folded = false,
hidden = false,
},
untracked = {
folded = false,
hidden = false,
},
bookmarks = {
folded = true,
hidden = false,
show_deleted = true,
show_remote = true,
},
recent = {
folded = true,
hidden = false,
},
},
mappings = {
commit_editor = {
["q"] = "Close",
["<c-c><c-c>"] = "Submit",
["<c-c><c-k>"] = "Abort",
["<m-p>"] = "PrevMessage",
["<m-n>"] = "NextMessage",
["<m-r>"] = "ResetMessage",
},
commit_editor_I = {
["<c-c><c-c>"] = "Submit",
["<c-c><c-k>"] = "Abort",
},
finder = {
["<cr>"] = "Select",
["<c-c>"] = "Close",
["<esc>"] = "Close",
["<c-n>"] = "Next",
["<c-p>"] = "Previous",
["<down>"] = "Next",
["<up>"] = "Previous",
["<tab>"] = "InsertCompletion",
["<c-y>"] = "CopySelection",
["<space>"] = "MultiselectToggleNext",
["<s-space>"] = "MultiselectTogglePrevious",
["<c-j>"] = "NOP",
["<ScrollWheelDown>"] = "ScrollWheelDown",
["<ScrollWheelUp>"] = "ScrollWheelUp",
["<ScrollWheelLeft>"] = "NOP",
["<ScrollWheelRight>"] = "NOP",
["<LeftMouse>"] = "MouseClick",
["<2-LeftMouse>"] = "NOP",
},
-- Setting any of these to `false` will disable the mapping.
popup = {
["?"] = "HelpPopup",
["b"] = "BookmarkPopup",
["c"] = "CommitPopup",
["d"] = "DiffPopup",
["f"] = "FetchPopup",
["l"] = "LogPopup",
["m"] = "RemotePopup",
["p"] = "PushPopup",
["r"] = "RebasePopup",
["s"] = "SquashPopup",
["u"] = "UndoPopup",
["w"] = "WorkspacePopup",
},
status = {
["j"] = "MoveDown",
["k"] = "MoveUp",
["o"] = "OpenTree",
["q"] = "Close",
["I"] = "InitRepo",
["1"] = "Depth1",
["2"] = "Depth2",
["3"] = "Depth3",
["4"] = "Depth4",
["Q"] = "Command",
["<tab>"] = "Toggle",
["za"] = "Toggle",
["zo"] = "OpenFold",
["zc"] = "CloseFold",
["zC"] = "Depth1",
["zO"] = "Depth4",
["x"] = "Discard",
["K"] = "Untrack",
["R"] = "Rename",
["y"] = "ShowRefs",
["$"] = "CommandHistory",
["Y"] = "YankSelected",
["gp"] = "GoToParentRepo",
["<c-r>"] = "RefreshBuffer",
["<cr>"] = "GoToFile",
["<s-cr>"] = "PeekFile",
["<c-v>"] = "VSplitOpen",
["<c-x>"] = "SplitOpen",
["<c-t>"] = "TabOpen",
["{"] = "GoToPreviousHunkHeader",
["}"] = "GoToNextHunkHeader",
["[c"] = "OpenOrScrollUp",
["]c"] = "OpenOrScrollDown",
["<c-k>"] = "PeekUp",
["<c-j>"] = "PeekDown",
["<c-n>"] = "NextSection",
["<c-p>"] = "PreviousSection",
},
},
}
The following popup menus are available from the status buffer (press ? for the help popup to see all keybindings):
| Key | Popup | Description |
|---|---|---|
? | Help | Show available keybindings |
b | Bookmark | Create, move, delete, forget, rename, track/untrack bookmarks. Move bookmark to another bookmark. |
c | Change | Commit, new change, new on revision/bookmark, new before, describe, edit, abandon, duplicate, revert. Uppercase variants move bookmarks forward. |
d | Diff | View diffs (working copy, range, trunk to working copy, specific change, diffedit) |
f | Fetch | Fetch from remotes |
l | Log | View log with revset support |
m | Remote | Add, remove, rename remotes |
p | Push | Push bookmarks to remotes |
r | Rebase | Rebase changes (single, range, onto revision) |
s | Squash | Squash changes into parent |
u | Undo | Undo/redo jj operations |
w | Workspace | Add, delete, forget, rename, list workspaces. Quick-add to worktrees directory. |
Many popups will use whatever is currently under the cursor or selected as input for an action. For example, to rebase a range of changes from the log view, a linewise visual selection can be made, and the rebase action will apply to that selection.
The status buffer shows:
<tab>)*)These keybindings work directly in the status buffer on whatever is under your cursor (a commit in Recent Changes, a bookmark, etc.):
| Key | Action | Description |
|---|---|---|
D | Describe | Edit the description of the change under cursor |
E | Edit | Switch your working copy to the change under cursor |
O | New change on | Create a new change on top of the revision under cursor |
B | New change before | Insert a new change before the revision under cursor |
F | Forget bookmark | Forget the bookmark under cursor (or track a remote bookmark) |
o | Open in browser | Open the line under cursor on GitHub/GitLab: the repo on the project header, the open PR on a line annotated with one, otherwise the commit |
x | Discard / Abandon | Discard file changes, abandon the change (or divergent variant) under cursor, or delete a bookmark under cursor |
[!NOTE] Keybind convention: lowercase keys open popups and pickers; capital letters are contextual actions that act on whatever is under the cursor.
Migrating from older versions: the Squash, Workspace, Push, and Remote popups moved from
S/W/P/Mtos/w/p/m. TheA(abandon current change) andN(new change) bindings were removed — usexto abandon the change under the cursor,c ato abandon via picker, andc n/O/Bto create new changes. Custommappings.popupconfigs are unaffected.
In git you'd run git fetch && git checkout feature-branch. In jj, bookmarks are the equivalent of branches:
f to open the Fetch popup, then f to fetch all remotesname@origin)O to create a new change on top of it — Neojj automatically tracks the remote bookmark for youAlternatively, from the Change popup (c):
p to pick a bookmark from a fuzzy finder and create a new change on ito to pick any revision from a fuzzy finderCLI equivalent: jj git fetch && jj bookmark track <name>@origin && jj new <bookmark-name>
In git you'd create a branch, commit, and push. In jj:
c then c to commit (opens an editor for the description), or c then D to describe inlineb, then c to create a bookmark on your changep to open the Push popup and push the bookmarkTo keep working after committing, press c then n to create a new change on top.
If you want bookmarks to automatically follow your new changes, use the uppercase variants: c then C (commit + move bookmarks) or c then N (new change + move bookmarks).
CLI equivalent: jj commit -m "msg" && jj bookmark create my-branch && jj git push --bookmark my-branch
You don't need to stash in jj. Every change is automatically its own "stash." To set aside current work:
c then n to create a new change — your previous work stays in the parent changec then e to edit back to your original change when you're ready to resumeSince there's no staging area, your working copy changes are always safely captured.
CLI equivalent: jj new (to set aside) then jj edit <change-id> (to resume)
In git you'd run git rebase main. In jj:
ff to fetch the latest changesr to open the Rebase popuph to rebase here (@) onto a selected revision, or t to rebase the whole stack onto trunkIf you have a chain of changes, use s in the rebase popup to rebase a source and all its descendants.
CLI equivalent: jj git fetch && jj rebase -d main
In jj, conflicts don't block operations — they're recorded in the commit. You can resolve them whenever you want:
CLI equivalent: jj resolve (to use a merge tool) or edit files directly
In jj, the equivalent of cherry-pick is "duplicate":
c to open the Change popupu to duplicate — pick a change from the fuzzy finderCLI equivalent: jj duplicate <change-id>
Quickest way (from status buffer): Move your cursor to a commit in Recent Changes or a bookmark in the Bookmarks section, then press O. This creates a new change directly on top of that revision.
From the Change popup (c):
o — pick any revision from a fuzzy finderO — same, but also moves bookmarks forwardp — pick a bookmark from a fuzzy finderP — same, but also moves bookmarks forwardSometimes you need to insert a fix before an existing change rather than on top of it.
From status buffer: Move your cursor to the target revision and press B.
From the Change popup (c): Press b to pick a revision from a fuzzy finder.
This runs jj new --insert-before, which creates a new change whose child is the target revision.
When you create new changes with c then n, bookmarks stay on the old change. If you want bookmarks to follow along:
c then N — new change + move bookmarks forwardc then C — commit + move bookmarks forwardc then O — new change on revision + move bookmarks forwardc then P — new change on bookmark + move bookmarks forwardThe uppercase variants move any bookmarks that were on the old @ to the new @, and bookmarks from the old @- to the new @-.
Splitting: Use jj split from the command line to break a change into two. Neojj will support this in a future release.
Squashing (s popup): Merge changes together.
s — squash the current change into its parentS — squash into a specific revision (picked from fuzzy finder)a — absorb changes into prior commits automaticallyIn jj, you can edit any change in your history, not just the latest one:
E to edit that change — your working copy switches to itc then n to create a new change when done, or E on another commit to jump elsewherePress D on any commit to edit its description without switching to it.
Use [c and ]c to scroll through a commit's diff in a side panel without leaving the status buffer.
Neojj includes full support for jj workspaces via the w popup:
a) / Add at revision (A) — create a new workspace at a chosen path (defaults to parent of repo root)q) / Quick add at revision (Q) — instantly create a workspace with a random name under a configurable worktrees directory (default ~/.worktrees)f) — stop tracking a workspace (files stay on disk)d) — forget and remove the workspace directoryr) — rename the current workspacel) — list all workspaces with pathsu) — recover a stale workspaceConfigure hooks to automatically open new workspaces:
neojj.setup {
-- Open a new tmux window in the workspace directory
workspace_open_command = "tmux new-window -c {path}",
-- Run a command in the workspace before opening (e.g., install deps)
workspace_initialize_command = nil,
-- Base directory for quick-add worktrees
workspace_worktrees_directory = "~/.worktrees",
}
See the built-in documentation for a comprehensive list of highlight groups. If your theme doesn't style a particular group, we'll try our best to do a nice job.
Neojj emits the following events:
| Event | Description | Event Data |
|---|---|---|
NeojjStatusRefreshed | Status has been reloaded | {} |
NeojjCommitComplete | Commit has been created | {} |
NeojjDescribeComplete | Description has been updated | {} |
NeojjNewChangeComplete | New change has been created | {} |
NeojjSquashComplete | Squash has completed | {} |
NeojjPushComplete | Push has completed | {} |
NeojjFetchComplete | Fetch has completed | {} |
NeojjBookmarkCreate | Bookmark was created | { bookmark_name: string } |
NeojjBookmarkDelete | Bookmark was deleted | { bookmark_name: string } |
NeojjRebaseComplete | A rebase finished | { commit: string, status: "ok"|"conflict" } |
NeojjAbandonComplete | A change was abandoned | { change_id: string } |
Neojj is a hard fork of Neogit. Thanks to the Neogit contributors for building the foundation this project is based on. This would not be possible without their work, and I personally use Neogit religiously for all things git.
(top 30 of 187)
Lua
99.2%
An interactive and powerful Jujutsu interface for Neovim, inspired by Magit, forked from Neogit
Lua
74
4,745 commits
updated Sep 16, 2026
Neojj is a hard fork of Neogit, adapted to work with jj (Jujutsu VCS) instead of git.
Maintainer: Nicholas Zolton
Note: Some features (specifically diffview and codediff integration) currently require a git-colocated repository. Work is underway to remove this dependency.
Requires jj (Jujutsu VCS) to be installed and available on your PATH.
Here's an example spec for Lazy, but you're free to use whichever plugin manager suits you.
{
"NicholasZolton/neojj",
version = "^1.0.0", -- recommended: track 1.x releases (see Versioning below)
lazy = true,
dependencies = {
"nvim-lua/plenary.nvim", -- required
-- Only one of these is needed.
"sindrets/diffview.nvim", -- optional
"esmuellert/codediff.nvim", -- optional
-- Only one of these is needed.
"nvim-telescope/telescope.nvim", -- optional
"ibhagwan/fzf-lua", -- optional
"nvim-mini/mini.pick", -- optional
"folke/snacks.nvim", -- optional
},
cmd = "Neojj",
keys = {
{ "<leader>gg", "<cmd>Neojj<cr>", desc = "Show Neojj UI" }
}
}
NeoJJ follows Semantic Versioning: breaking changes only
land in major releases. With version = "^1.0.0", lazy.nvim installs the
latest 1.x tag — you get new features and fixes on :Lazy update, but a
breaking 2.0 requires you to bump the pin yourself. Drop the version line
to track main directly (bleeding edge, may break without warning). Each
release is documented in CHANGELOG.md.
You can either open Neojj by using the Neojj command:
:Neojj " Open the status buffer in a new tab
:Neojj cwd=<cwd> " Use a different repository path
:Neojj cwd=%:p:h " Uses the repository of the current file
:Neojj kind=<kind> " Open specified popup directly
:Neojj commit " Open commit popup
:Neojj bookmark " Open bookmark popup
:Neojj workspace " Open workspace popup
" Map it to a key
nnoremap <leader>gg <cmd>Neojj<cr>
-- Or via lua api
vim.keymap.set("n", "<leader>gg", "<cmd>Neojj<cr>", { desc = "Open Neojj UI" })
Or using the lua api:
local neojj = require('neojj')
-- open using defaults
neojj.open()
-- open a specific popup
neojj.open({ "commit" })
-- open as a split
neojj.open({ kind = "split" })
-- open with different project
neojj.open({ cwd = "~" })
-- You can map this to a key
vim.keymap.set("n", "<leader>gg", neojj.open, { desc = "Open Neojj UI" })
-- Wrap in a function to pass additional arguments
vim.keymap.set(
"n",
"<leader>gg",
function() neojj.open({ kind = "split" }) end,
{ desc = "Open Neojj UI" }
)
The kind option can be one of the following values:
tab (default)replacesplitsplit_abovesplit_above_allsplit_belowsplit_below_allvsplitfloatingauto (vsplit if window would have 80 cols, otherwise split)If you are coming from git, here are some key differences in jj:
jj undo.You can configure Neojj by running the require('neojj').setup {} function, passing a table as the argument.
local neojj = require("neojj")
neojj.setup {
-- Path to jj binary. "auto" = auto-detect (resolves shims for performance).
jj_binary = "auto",
-- Highlight changed words inside paired deleted/added lines.
word_diff_highlight = true,
-- Hides the hints at the top of the status buffer
disable_hint = false,
-- Shows the project name at the top of the status buffer
show_project_header = true,
-- Disables changing the buffer highlights based on where the cursor is.
disable_context_highlighting = false,
-- Uses Tree-sitter parsers to apply syntax highlighting inside diff hunks.
treesitter_diff_highlight = false,
-- Highlights changed words within paired deletion and addition lines.
word_diff_highlight = true,
-- Disables signs for sections/items/hunks
disable_signs = false,
-- Changes what mode the Commit Editor starts in. `true` will leave nvim in normal mode, `false` will change nvim to
-- insert mode, and `"auto"` will change nvim to insert mode IF the commit message is empty, otherwise leaving it in
-- normal mode.
disable_insert_on_commit = "auto",
-- When enabled, will watch the `.jj/` directory for changes and refresh the status buffer in response to filesystem
-- events.
filewatcher = {
enabled = true,
},
-- "ascii" is the graph the jj CLI generates
-- "unicode" is a unicode graph style
graph_style = "ascii",
-- Show relative date by default. When set, use `strftime` to display dates
commit_date_format = nil,
log_date_format = nil,
-- Show message with spinning animation when a jj command is running.
process_spinner = false,
-- Allows a different telescope sorter. Defaults to 'fuzzy_with_index_bias'. The example below will use the native fzf
-- sorter instead. By default, this function returns `nil`.
telescope_sorter = function()
return require("telescope").extensions.fzf.native_fzf_sorter()
end,
-- Persist the values of switches/options within and across sessions
remember_settings = true,
-- Scope persisted settings on a per-project basis
use_per_project_settings = true,
-- Table of settings to never persist. Uses format "Filetype--cli-value"
ignored_settings = {},
-- Configure highlight group features
highlight = {
italic = true,
bold = true,
underline = true
},
-- Set to false if you want to be responsible for creating _ALL_ keymappings
use_default_keymaps = true,
-- Change the default way of opening Neojj
kind = "tab",
-- Floating window style
floating = {
relative = "editor",
width = 0.8,
height = 0.7,
style = "minimal",
border = "rounded",
},
-- Disable line numbers
disable_line_numbers = true,
-- Disable relative line numbers
disable_relative_line_numbers = true,
-- The time after which an output console is shown for slow running commands
console_timeout = 2000,
-- Automatically show console if a command takes more than console_timeout milliseconds
auto_show_console = true,
-- If `auto_show_console` is enabled, specify "output" (default) to show
-- the console always, or "error" to auto-show the console only on error
auto_show_console_on = "output",
-- Automatically close the console if the process exits with a 0 (success) status
auto_close_console = true,
notification_icon = "",
-- Shell command to run in new workspace directory after creation. {path} is replaced with the workspace path.
workspace_open_command = nil,
-- Shell command to initialize a new workspace before opening. {path} is replaced with the workspace path.
workspace_initialize_command = nil,
-- Base directory for quick-add worktrees (used by workspace popup's quick add action)
workspace_worktrees_directory = "~/.worktrees",
status = {
show_head_commit_hash = true,
recent_commit_count = 10,
HEAD_padding = 10,
HEAD_folded = false,
mode_padding = 3,
mode_text = {
M = "modified",
N = "new file",
A = "added",
D = "deleted",
C = "copied",
R = "renamed",
["?"] = "",
},
},
commit_editor = {
kind = "tab",
show_diff = true,
diff_split_kind = "split",
spell_check = true,
},
commit_select_view = {
kind = "tab",
},
commit_view = {
kind = "vsplit",
verify_commit = vim.fn.executable("gpg") == 1,
},
log_view = {
kind = "tab",
},
reflog_view = {
kind = "tab",
},
preview_buffer = {
kind = "floating_console",
},
popup = {
kind = "split",
},
signs = {
-- { CLOSED, OPENED }
hunk = { "", "" },
item = { ">", "v" },
section = { ">", "v" },
},
-- Each Integration is auto-detected through plugin presence, however, it can be disabled by setting to `false`
integrations = {
-- If enabled, use telescope for menu selection rather than vim.ui.select.
-- Allows multi-select and some things that vim.ui.select doesn't.
telescope = nil,
-- Neojj only provides inline diffs. If you want a more traditional way to look at diffs, you can use `diffview`.
-- The diffview integration enables the diff popup.
--
-- Requires you to have `sindrets/diffview.nvim` installed.
diffview = nil,
-- Alternative diff viewer integration.
-- Requires you to have `esmuellert/codediff.nvim` installed.
codediff = nil,
-- If enabled, uses fzf-lua for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `ibhagwan/fzf-lua` installed.
fzf_lua = nil,
-- If enabled, uses mini.pick for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `echasnovski/mini.pick` installed.
mini_pick = nil,
-- If enabled, uses snacks.picker for menu selection. If the telescope integration
-- is also selected then telescope is used instead
-- Requires you to have `folke/snacks.nvim` installed.
snacks = nil,
},
-- Which diff viewer to use. nil = auto-detect (tries diffview first, then codediff).
-- Can be "diffview" or "codediff".
diff_viewer = nil,
-- Place new CodeDiff tabs before or after the current tab.
codediff_tab_position = "after",
-- Forge (GitHub, ...) integration. When enabled and the forge CLI (`gh`) is
-- installed, bookmarks with a matching open PR are annotated with the PR
-- number, and "o" on such a line opens the PR instead of the commit.
forge = {
pr_integration = true,
-- Extra remote hosts per provider, e.g. for GitHub Enterprise:
-- hosts = { github = { "git.yourcompany.com" } },
hosts = {},
},
sections = {
files = {
folded = false,
hidden = false,
},
conflicts = {
folded = false,
hidden = false,
},
untracked = {
folded = false,
hidden = false,
},
bookmarks = {
folded = true,
hidden = false,
show_deleted = true,
show_remote = true,
},
recent = {
folded = true,
hidden = false,
},
},
mappings = {
commit_editor = {
["q"] = "Close",
["<c-c><c-c>"] = "Submit",
["<c-c><c-k>"] = "Abort",
["<m-p>"] = "PrevMessage",
["<m-n>"] = "NextMessage",
["<m-r>"] = "ResetMessage",
},
commit_editor_I = {
["<c-c><c-c>"] = "Submit",
["<c-c><c-k>"] = "Abort",
},
finder = {
["<cr>"] = "Select",
["<c-c>"] = "Close",
["<esc>"] = "Close",
["<c-n>"] = "Next",
["<c-p>"] = "Previous",
["<down>"] = "Next",
["<up>"] = "Previous",
["<tab>"] = "InsertCompletion",
["<c-y>"] = "CopySelection",
["<space>"] = "MultiselectToggleNext",
["<s-space>"] = "MultiselectTogglePrevious",
["<c-j>"] = "NOP",
["<ScrollWheelDown>"] = "ScrollWheelDown",
["<ScrollWheelUp>"] = "ScrollWheelUp",
["<ScrollWheelLeft>"] = "NOP",
["<ScrollWheelRight>"] = "NOP",
["<LeftMouse>"] = "MouseClick",
["<2-LeftMouse>"] = "NOP",
},
-- Setting any of these to `false` will disable the mapping.
popup = {
["?"] = "HelpPopup",
["b"] = "BookmarkPopup",
["c"] = "CommitPopup",
["d"] = "DiffPopup",
["f"] = "FetchPopup",
["l"] = "LogPopup",
["m"] = "RemotePopup",
["p"] = "PushPopup",
["r"] = "RebasePopup",
["s"] = "SquashPopup",
["u"] = "UndoPopup",
["w"] = "WorkspacePopup",
},
status = {
["j"] = "MoveDown",
["k"] = "MoveUp",
["o"] = "OpenTree",
["q"] = "Close",
["I"] = "InitRepo",
["1"] = "Depth1",
["2"] = "Depth2",
["3"] = "Depth3",
["4"] = "Depth4",
["Q"] = "Command",
["<tab>"] = "Toggle",
["za"] = "Toggle",
["zo"] = "OpenFold",
["zc"] = "CloseFold",
["zC"] = "Depth1",
["zO"] = "Depth4",
["x"] = "Discard",
["K"] = "Untrack",
["R"] = "Rename",
["y"] = "ShowRefs",
["$"] = "CommandHistory",
["Y"] = "YankSelected",
["gp"] = "GoToParentRepo",
["<c-r>"] = "RefreshBuffer",
["<cr>"] = "GoToFile",
["<s-cr>"] = "PeekFile",
["<c-v>"] = "VSplitOpen",
["<c-x>"] = "SplitOpen",
["<c-t>"] = "TabOpen",
["{"] = "GoToPreviousHunkHeader",
["}"] = "GoToNextHunkHeader",
["[c"] = "OpenOrScrollUp",
["]c"] = "OpenOrScrollDown",
["<c-k>"] = "PeekUp",
["<c-j>"] = "PeekDown",
["<c-n>"] = "NextSection",
["<c-p>"] = "PreviousSection",
},
},
}
The following popup menus are available from the status buffer (press ? for the help popup to see all keybindings):
| Key | Popup | Description |
|---|---|---|
? | Help | Show available keybindings |
b | Bookmark | Create, move, delete, forget, rename, track/untrack bookmarks. Move bookmark to another bookmark. |
c | Change | Commit, new change, new on revision/bookmark, new before, describe, edit, abandon, duplicate, revert. Uppercase variants move bookmarks forward. |
d | Diff | View diffs (working copy, range, trunk to working copy, specific change, diffedit) |
f | Fetch | Fetch from remotes |
l | Log | View log with revset support |
m | Remote | Add, remove, rename remotes |
p | Push | Push bookmarks to remotes |
r | Rebase | Rebase changes (single, range, onto revision) |
s | Squash | Squash changes into parent |
u | Undo | Undo/redo jj operations |
w | Workspace | Add, delete, forget, rename, list workspaces. Quick-add to worktrees directory. |
Many popups will use whatever is currently under the cursor or selected as input for an action. For example, to rebase a range of changes from the log view, a linewise visual selection can be made, and the rebase action will apply to that selection.
The status buffer shows:
<tab>)*)These keybindings work directly in the status buffer on whatever is under your cursor (a commit in Recent Changes, a bookmark, etc.):
| Key | Action | Description |
|---|---|---|
D | Describe | Edit the description of the change under cursor |
E | Edit | Switch your working copy to the change under cursor |
O | New change on | Create a new change on top of the revision under cursor |
B | New change before | Insert a new change before the revision under cursor |
F | Forget bookmark | Forget the bookmark under cursor (or track a remote bookmark) |
o | Open in browser | Open the line under cursor on GitHub/GitLab: the repo on the project header, the open PR on a line annotated with one, otherwise the commit |
x | Discard / Abandon | Discard file changes, abandon the change (or divergent variant) under cursor, or delete a bookmark under cursor |
[!NOTE] Keybind convention: lowercase keys open popups and pickers; capital letters are contextual actions that act on whatever is under the cursor.
Migrating from older versions: the Squash, Workspace, Push, and Remote popups moved from
S/W/P/Mtos/w/p/m. TheA(abandon current change) andN(new change) bindings were removed — usexto abandon the change under the cursor,c ato abandon via picker, andc n/O/Bto create new changes. Custommappings.popupconfigs are unaffected.
In git you'd run git fetch && git checkout feature-branch. In jj, bookmarks are the equivalent of branches:
f to open the Fetch popup, then f to fetch all remotesname@origin)O to create a new change on top of it — Neojj automatically tracks the remote bookmark for youAlternatively, from the Change popup (c):
p to pick a bookmark from a fuzzy finder and create a new change on ito to pick any revision from a fuzzy finderCLI equivalent: jj git fetch && jj bookmark track <name>@origin && jj new <bookmark-name>
In git you'd create a branch, commit, and push. In jj:
c then c to commit (opens an editor for the description), or c then D to describe inlineb, then c to create a bookmark on your changep to open the Push popup and push the bookmarkTo keep working after committing, press c then n to create a new change on top.
If you want bookmarks to automatically follow your new changes, use the uppercase variants: c then C (commit + move bookmarks) or c then N (new change + move bookmarks).
CLI equivalent: jj commit -m "msg" && jj bookmark create my-branch && jj git push --bookmark my-branch
You don't need to stash in jj. Every change is automatically its own "stash." To set aside current work:
c then n to create a new change — your previous work stays in the parent changec then e to edit back to your original change when you're ready to resumeSince there's no staging area, your working copy changes are always safely captured.
CLI equivalent: jj new (to set aside) then jj edit <change-id> (to resume)
In git you'd run git rebase main. In jj:
ff to fetch the latest changesr to open the Rebase popuph to rebase here (@) onto a selected revision, or t to rebase the whole stack onto trunkIf you have a chain of changes, use s in the rebase popup to rebase a source and all its descendants.
CLI equivalent: jj git fetch && jj rebase -d main
In jj, conflicts don't block operations — they're recorded in the commit. You can resolve them whenever you want:
CLI equivalent: jj resolve (to use a merge tool) or edit files directly
In jj, the equivalent of cherry-pick is "duplicate":
c to open the Change popupu to duplicate — pick a change from the fuzzy finderCLI equivalent: jj duplicate <change-id>
Quickest way (from status buffer): Move your cursor to a commit in Recent Changes or a bookmark in the Bookmarks section, then press O. This creates a new change directly on top of that revision.
From the Change popup (c):
o — pick any revision from a fuzzy finderO — same, but also moves bookmarks forwardp — pick a bookmark from a fuzzy finderP — same, but also moves bookmarks forwardSometimes you need to insert a fix before an existing change rather than on top of it.
From status buffer: Move your cursor to the target revision and press B.
From the Change popup (c): Press b to pick a revision from a fuzzy finder.
This runs jj new --insert-before, which creates a new change whose child is the target revision.
When you create new changes with c then n, bookmarks stay on the old change. If you want bookmarks to follow along:
c then N — new change + move bookmarks forwardc then C — commit + move bookmarks forwardc then O — new change on revision + move bookmarks forwardc then P — new change on bookmark + move bookmarks forwardThe uppercase variants move any bookmarks that were on the old @ to the new @, and bookmarks from the old @- to the new @-.
Splitting: Use jj split from the command line to break a change into two. Neojj will support this in a future release.
Squashing (s popup): Merge changes together.
s — squash the current change into its parentS — squash into a specific revision (picked from fuzzy finder)a — absorb changes into prior commits automaticallyIn jj, you can edit any change in your history, not just the latest one:
E to edit that change — your working copy switches to itc then n to create a new change when done, or E on another commit to jump elsewherePress D on any commit to edit its description without switching to it.
Use [c and ]c to scroll through a commit's diff in a side panel without leaving the status buffer.
Neojj includes full support for jj workspaces via the w popup:
a) / Add at revision (A) — create a new workspace at a chosen path (defaults to parent of repo root)q) / Quick add at revision (Q) — instantly create a workspace with a random name under a configurable worktrees directory (default ~/.worktrees)f) — stop tracking a workspace (files stay on disk)d) — forget and remove the workspace directoryr) — rename the current workspacel) — list all workspaces with pathsu) — recover a stale workspaceConfigure hooks to automatically open new workspaces:
neojj.setup {
-- Open a new tmux window in the workspace directory
workspace_open_command = "tmux new-window -c {path}",
-- Run a command in the workspace before opening (e.g., install deps)
workspace_initialize_command = nil,
-- Base directory for quick-add worktrees
workspace_worktrees_directory = "~/.worktrees",
}
See the built-in documentation for a comprehensive list of highlight groups. If your theme doesn't style a particular group, we'll try our best to do a nice job.
Neojj emits the following events:
| Event | Description | Event Data |
|---|---|---|
NeojjStatusRefreshed | Status has been reloaded | {} |
NeojjCommitComplete | Commit has been created | {} |
NeojjDescribeComplete | Description has been updated | {} |
NeojjNewChangeComplete | New change has been created | {} |
NeojjSquashComplete | Squash has completed | {} |
NeojjPushComplete | Push has completed | {} |
NeojjFetchComplete | Fetch has completed | {} |
NeojjBookmarkCreate | Bookmark was created | { bookmark_name: string } |
NeojjBookmarkDelete | Bookmark was deleted | { bookmark_name: string } |
NeojjRebaseComplete | A rebase finished | { commit: string, status: "ok"|"conflict" } |
NeojjAbandonComplete | A change was abandoned | { change_id: string } |
Neojj is a hard fork of Neogit. Thanks to the Neogit contributors for building the foundation this project is based on. This would not be possible without their work, and I personally use Neogit religiously for all things git.
(top 30 of 187)
Lua
99.2%