picker-based forge workflow for Neovim
Find a file
2026-03-28 15:05:55 -04:00
.github ci: release workflow 2026-03-28 14:57:15 -04:00
doc docs: add compatibility matrix and capabilities to vimdoc 2026-03-28 14:37:00 -04:00
lua/forge feat: tests and better vim validate 2026-03-28 15:05:55 -04:00
plugin feat: add capabilities system and per-forge compatibility 2026-03-28 14:36:32 -04:00
scripts ci: fix nix fmt flag and prettier formatting 2026-03-28 14:37:21 -04:00
spec feat: tests and better vim validate 2026-03-28 15:05:55 -04:00
.busted feat: tests and better vim validate 2026-03-28 15:05:55 -04:00
.editorconfig feat: initial commit 2026-03-27 16:46:28 -04:00
.gitignore feat: initial commit 2026-03-27 16:46:28 -04:00
.luarc.json feat: initial commit 2026-03-27 16:46:28 -04:00
.pre-commit-config.yaml ci: update pre-commit 2026-03-27 19:15:47 -04:00
.prettierignore feat: initial commit 2026-03-27 16:46:28 -04:00
.prettierrc feat: initial commit 2026-03-27 16:46:28 -04:00
flake.lock more tweaks 2026-03-27 18:17:12 -04:00
flake.nix more tweaks 2026-03-27 18:17:12 -04:00
forge.nvim-scm-1.rockspec feat: tests and better vim validate 2026-03-28 15:05:55 -04:00
LICENSE feat: initial commit 2026-03-27 16:46:28 -04:00
README.md ci: format 2026-03-28 00:53:15 -04:00
selene.toml feat: initial commit 2026-03-27 16:46:28 -04:00
stylua.toml feat: initial commit 2026-03-27 16:46:28 -04:00
vim.yaml feat: initial commit 2026-03-27 16:46:28 -04:00

forge.nvim

Forge-agnostic git workflow for Neovim

PR, issue, and CI workflows across GitHub, GitLab, and more — without leaving your editor.

Features

  • Automatic forge detection from git remote (GitHub via gh, GitLab via glab, Codeberg/Gitea/Forgejo via tea)
  • PR lifecycle: list, create (compose buffer with template discovery, diff stat, reviewers), checkout, worktree, review diff, merge, approve, close/reopen, draft toggle
  • Issue management: list, browse, close/reopen, state filtering
  • CI/CD: view runs per-branch or repo-wide, stream logs, filter by status
  • Code review via diffs.nvim with unified/split toggle and quickfix navigation
  • Commit and branch browsing with checkout, diff, and URL generation
  • File/line permalink generation and yanking (commit and branch URLs)
  • fzf-lua pickers with contextual keybinds
  • Pluggable source registration for custom or self-hosted forges

Dependencies

  • Neovim 0.10.0+
  • fzf-lua
  • At least one forge CLI:
    • gh for GitHub
    • glab for GitLab
    • tea for Codeberg/Gitea/Forgejo
  • vim-fugitive (optional, for fugitive keymaps and split diff)
  • diffs.nvim (optional, for review mode)

Installation

lazy.nvim

{
  'barrettruth/forge.nvim',
  dependencies = { 'ibhagwan/fzf-lua' },
}

mini.deps

MiniDeps.add({
  source = 'barrettruth/forge.nvim',
  depends = { 'ibhagwan/fzf-lua' },
})

luarocks

luarocks install forge.nvim

Manual

git clone https://github.com/barrettruth/forge.nvim \
  ~/.local/share/nvim/site/pack/plugins/start/forge.nvim

Usage

forge.nvim works through two entry points: the :Forge command and the <c-g> picker.

:Forge with no arguments (or <c-g>) opens the top-level picker — PRs, issues, CI, commits, branches, worktrees, and browse actions. Each sub-picker has contextual keybinds shown in the fzf header.

PR creation opens a compose buffer (markdown) pre-filled from commit messages and repo templates. First line is the title, everything after the blank line is the body. Draft, reviewers, and base branch are set in the HTML comment block below. Write (:w) to push and create.

Configuration

Configure via vim.g.forge. All fields are optional — defaults shown below.

vim.g.forge = {
  ci = { lines = 10000 },
  sources = {},
  keys = {
    picker = '<c-g>',
    next_qf = ']q',  prev_qf = '[q',
    next_loc = ']l',  prev_loc = '[l',
    review_toggle = 's',
    terminal_open = 'gx',
    fugitive = {
      create = 'cpr', create_draft = 'cpd',
      create_fill = 'cpf', create_web = 'cpw',
    },
  },
  picker_keys = {
    pr = {
      checkout = 'default', diff = 'ctrl-d', worktree = 'ctrl-w',
      checks = 'ctrl-t', browse = 'ctrl-x', manage = 'ctrl-e',
      create = 'ctrl-a', toggle = 'ctrl-o', refresh = 'ctrl-r',
    },
    issue = { browse = 'default', close_reopen = 'ctrl-s', toggle = 'ctrl-o', refresh = 'ctrl-r' },
    checks = { log = 'default', browse = 'ctrl-x', failed = 'ctrl-f', passed = 'ctrl-p', running = 'ctrl-n', all = 'ctrl-a' },
    ci = { log = 'default', browse = 'ctrl-x', refresh = 'ctrl-r' },
    commits = { checkout = 'default', diff = 'ctrl-d', browse = 'ctrl-x', yank = 'ctrl-y' },
    branches = { diff = 'ctrl-d', browse = 'ctrl-x' },
  },
  display = {
    icons = { open = '+', merged = 'm', closed = 'x', pass = '*', fail = 'x', pending = '~', skip = '-', unknown = '?' },
    widths = { title = 45, author = 15, name = 35, branch = 25 },
    limits = { pulls = 100, issues = 100, runs = 30 },
  },
}

Set keys = false to disable all keymaps. Set picker_keys = false to disable all picker keybinds. Set any individual key to false to disable it.

Examples

Disable quickfix/loclist keymaps:

vim.g.forge = {
  keys = { next_qf = false, prev_qf = false, next_loc = false, prev_loc = false },
}

Nerd font icons:

vim.g.forge = {
  display = {
    icons = { open = '', merged = '', closed = '', pass = '', fail = '', pending = '', skip = '', unknown = '' },
  },
}

Self-hosted GitLab:

vim.g.forge = {
  sources = { gitlab = { hosts = { 'gitlab.mycompany.com' } } },
}

Override PR picker bindings:

vim.g.forge = {
  picker_keys = { pr = { checkout = 'ctrl-o', diff = 'default' } },
}

Commands

:Forge with no arguments opens the top-level picker. Subcommands:

Command Description
:Forge pr List open PRs
:Forge pr --state={open,closed,all} List PRs by state
:Forge pr create [--draft] [--fill] [--web] Create PR
:Forge pr checkout {num} Checkout PR branch
:Forge pr diff {num} Review PR diff
:Forge pr worktree {num} Fetch PR into worktree
:Forge pr checks {num} Show PR checks
:Forge pr browse {num} Open PR in browser
:Forge pr manage {num} Merge/approve/close/draft actions
:Forge issue List all issues
:Forge issue --state={open,closed,all} List issues by state
:Forge issue browse {num} Open issue in browser
:Forge issue close {num} Close issue
:Forge issue reopen {num} Reopen issue
:Forge ci CI runs for current branch
:Forge ci --all CI runs for all branches
:Forge commit Browse commits
:Forge commit checkout {sha} Checkout commit
:Forge commit diff {sha} Review commit diff
:Forge commit browse {sha} Open commit in browser
:Forge branch Browse branches
:Forge branch diff {name} Review branch diff
:Forge branch browse {name} Open branch in browser
:Forge worktree List worktrees
:Forge browse [--root] [--commit] Open file/repo/commit in browser
:Forge yank [--commit] Yank permalink for file/line
:Forge review end End review session
:Forge review toggle Toggle split/unified review
:Forge cache clear Clear all caches

Keymaps

Global

Key Mode Description
<c-g> n, v Open forge picker
]q / [q n Next/prev quickfix entry (wraps)
]l / [l n Next/prev loclist entry (wraps)

Fugitive buffer

Active in fugitive filetype buffers when a forge is detected.

Key Description
cpr Create PR (compose buffer)
cpd Create draft PR
cpf Create PR from commits (no compose)
cpw Push and open web creation

Review

Active during a review session.

Key Description
s Toggle unified/split diff

Terminal (log buffers)

Active on CI/check log terminals when a URL is available.

Key Description
gx Open run/check in browser

Picker Actions

Keybinds shown in the fzf header. default = enter.

Picker Key Action
PR enter Checkout
ctrl-d Review diff
ctrl-w Worktree
ctrl-t Checks
ctrl-x Browse
ctrl-e Manage (merge/approve/close/draft)
ctrl-a Create new
ctrl-o Cycle state (open/closed/all)
ctrl-r Refresh
Issue enter Browse
ctrl-s Close/reopen
ctrl-o Cycle state
ctrl-r Refresh
Checks enter View log (tails if running)
ctrl-x Browse
ctrl-f / ctrl-p / ctrl-n Filter: failed / passed / running
ctrl-a Show all
CI enter View log (tails if running)
ctrl-x Browse
ctrl-r Refresh
Commits enter Checkout (detached)
ctrl-d Review diff
ctrl-x Browse
ctrl-y Yank hash
Branches ctrl-d Review diff
ctrl-x Browse

Custom Sources

Register a custom forge source for self-hosted or alternative platforms:

require('forge').register('mygitea', require('my_gitea_source'))

Route remotes to your source by host:

vim.g.forge = {
  sources = { mygitea = { hosts = { 'gitea.internal.dev' } } },
}

A source is a table implementing the forge.Forge interface. Required fields: name (string), cli (string, checked via executable()), kinds ({ issue, pr }), and labels ({ issue, pr, pr_one, pr_full, ci }).

Required methods (all receive self): list_pr_json_cmd, list_issue_json_cmd, pr_json_fields, issue_json_fields, view_web, browse, browse_root, browse_branch, browse_commit, checkout_cmd, yank_branch, yank_commit, fetch_pr, pr_base_cmd, pr_for_branch_cmd, checks_cmd, check_log_cmd, check_tail_cmd, list_runs_json_cmd, list_runs_cmd, normalize_run, run_log_cmd, run_tail_cmd, merge_cmd, approve_cmd, repo_info, pr_state, close_cmd, reopen_cmd, close_issue_cmd, reopen_issue_cmd, draft_toggle_cmd, create_pr_cmd, create_pr_web_cmd, default_branch_cmd, template_paths.

See lua/forge/github.lua, lua/forge/gitlab.lua, or lua/forge/codeberg.lua for complete implementations. The forge.Forge class definition with full type annotations is in lua/forge/init.lua.

Skeleton

local M = {
  name = 'mygitea',
  cli = 'tea',
  kinds = { issue = 'issues', pr = 'pulls' },
  labels = { issue = 'Issues', pr = 'PRs', pr_one = 'PR', pr_full = 'Pull Requests', ci = 'CI/CD' },
}

function M:list_pr_json_cmd(state)
  return { 'tea', 'pr', 'list', '--state', state, '--output', 'json' }
end

function M:pr_json_fields()
  return { number = 'number', title = 'title', branch = 'head', state = 'state', author = 'poster', created_at = 'created_at' }
end

return M

Health

Run :checkhealth forge to verify your setup. Checks for git, forge CLIs (gh, glab, tea), required plugins (fzf-lua), optional plugins (diffs.nvim, vim-fugitive), and any registered custom sources.

FAQ

Q: How do I create a PR? <c-g> -> Pull Requests -> ctrl-a to compose. Or from fugitive: cpr (compose), cpd (draft), cpf (instant), cpw (web).

Q: Does review mode require diffs.nvim? Yes. Without diffs.nvim, the diff action and review toggling are unavailable.

Q: How does forge detection work? forge.nvim reads the origin remote URL and matches against known hosts and any custom sources.<name>.hosts. The first match wins, and the CLI must be in $PATH.

Q: Can I use this with self-hosted GitLab/Gitea? Yes. Add your host to vim.g.forge.sources. See the examples.

Q: What does ctrl-o do in pickers? Cycles the state filter: open -> closed -> all -> open.

Q: How do I merge/approve/close a PR? ctrl-e on a PR in the picker opens the manage picker. Available actions depend on your repository permissions.

Q: Does this work without a forge remote? Partially. Commits, branches, and worktrees work in any git repo. PRs, issues, CI, and browse require a detected forge.