*forge.nvim.txt* Forge-agnostic git workflow for Neovim ============================================================================== INTRODUCTION *forge.nvim* forge.nvim provides PR, issue, and CI workflows across GitHub, GitLab, and Codeberg from inside Neovim. It detects the forge from your git remote and delegates to the appropriate CLI (`gh`, `glab`, or `tea`). Requirements: ~ - Neovim 0.10.0+ - fzf-lua (required) - One or more forge CLIs: - `gh` for GitHub - `glab` for GitLab - `tea` for Codeberg/Gitea/Forgejo - vim-fugitive (optional, for split review) - diffs.nvim (optional, for review mode) Run |:checkhealth| forge to verify CLIs and dependencies. ============================================================================== CONFIGURATION *forge-config* Configuration is set via the `vim.g.forge` global. All keys are optional; unset keys use defaults. >lua vim.g.forge = { ci = { lines = 5000 }, display = { icons = { open = '' } }, } < Top-level keys: ~ `ci` *forge-config-ci* `ci.lines` `integer` (default `10000`) Maximum number of log lines fetched for CI/check log output. `sources` *forge-config-sources* `table` (default `{}`) Per-source host overrides for forge detection. Keys are source names (e.g. `"github"`, `"gitlab"`, or a custom name). Each value has a `hosts` list of hostname substrings matched against the git remote. >lua vim.g.forge = { sources = { gitlab = { hosts = { 'git.internal.co' } }, myforgejo = { hosts = { 'forgejo.example.com' } }, }, } < `keys` *forge-config-keys* `table|false` (default shown below) Per-picker action bindings. Set to `false` to disable all picker-level actions. Use `""` to bind to enter. Values use vim keymap notation (e.g. `""`), which is translated to fzf-lua format internally. Defaults: >lua keys = { pr = { checkout = '', diff = '', worktree = '', ci = '', browse = '', manage = '', create = '', filter = '', refresh = '', }, issue = { browse = '', close = '', filter = '', refresh = '', }, ci = { log = '', browse = '', failed = '', passed = '', running = '', all = '', refresh = '', }, commits = { checkout = '', diff = '', browse = '', yank = '', }, branches = { diff = '', browse = '', }, } < `display` *forge-config-display* Controls icons, column widths, and fetch limits used in picker formatting. `display.icons` *forge-config-display-icons* `table` Status icons used in picker lines. Key Default Used for ~ `open` `"+"` Open PRs/issues `merged` `"m"` Merged PRs `closed` `"x"` Closed PRs/issues `pass` `"*"` Passed checks/runs `fail` `"x"` Failed checks/runs `pending` `"~"` In-progress checks/runs `skip` `"-"` Skipped/cancelled runs `unknown` `"?"` Unknown status `display.widths` *forge-config-display-widths* `table` Column widths (characters) for picker formatting. Key Default ~ `title` `45` `author` `15` `name` `35` `branch` `25` `display.limits` *forge-config-display-limits* `table` Maximum items fetched per list API call. Key Default ~ `pulls` `100` `issues` `100` `runs` `30` ============================================================================== COMMANDS *:Forge* `:Forge` *:Forge-no-args* Open the main forge picker (|forge-picker|). `:Forge pr` [{flags}] *:Forge-pr* Open the PR list picker. Defaults to open PRs. `--state=open` Show open PRs (default). `--state=closed` Show closed PRs. `--state=all` Show all PRs. `:Forge pr create` [{flags}] *:Forge-pr-create* Create a new PR. (no flags) Open the compose buffer (|forge-compose|). `--draft` Open compose buffer with draft pre-set. `--fill` Create instantly from commits (skip compose). `--web` Push and open the forge's web create-PR page. `--draft --fill` Create draft instantly. `:Forge pr checkout` {num} *:Forge-pr-checkout* Check out the branch for PR `{num}`. `:Forge pr diff` {num} *:Forge-pr-diff* Start a review for PR `{num}` (|forge-review|). `:Forge pr worktree` {num} *:Forge-pr-worktree* Fetch PR `{num}` and create a git worktree. `:Forge pr ci` {num} *:Forge-pr-ci* Open the CI checks picker for PR `{num}`. `:Forge pr browse` {num} *:Forge-pr-browse* Open PR `{num}` in the browser. `:Forge pr manage` {num} *:Forge-pr-manage* Open the management picker for PR `{num}` (approve, merge, close, reopen, draft toggle). `:Forge issue` [{flags}] *:Forge-issue* Open the issue list picker. Defaults to all issues. `--state=open` Show open issues. `--state=closed` Show closed issues. `--state=all` Show all issues (default). `:Forge issue browse` {num} *:Forge-issue-browse* Open issue `{num}` in the browser. `:Forge issue close` {num} *:Forge-issue-close* Close issue `{num}`. `:Forge issue reopen` {num} *:Forge-issue-reopen* Reopen issue `{num}`. `:Forge ci` [{flags}] *:Forge-ci* Open the CI runs picker. Defaults to current branch. `--all` Show runs for all branches. `:Forge commit` *:Forge-commit* Open the commit log picker. `:Forge commit checkout` {sha} *:Forge-commit-checkout* Check out commit `{sha}` (detached HEAD). `:Forge commit diff` {sha} *:Forge-commit-diff* Start a review for commit `{sha}`. `:Forge commit browse` {sha} *:Forge-commit-browse* Open commit `{sha}` on the forge. `:Forge branch` *:Forge-branch* Open the branch picker. `:Forge branch diff` {name} *:Forge-branch-diff* Start a review diffing against `{name}`. `:Forge branch browse` {name} *:Forge-branch-browse* Open branch `{name}` on the forge. `:Forge worktree` *:Forge-worktree* Open fzf-lua's git worktree picker. `:Forge browse` [{flags}] *:Forge-browse* Open the current file location on the forge. `--root` Open the repository root page. `--commit` Open the current HEAD commit. (no flags) Open the current file and line(s) on the current branch. In visual mode, the selected line range is included. `:Forge yank` [{flags}] *:Forge-yank* Yank a permalink URL to the `+` register. `--commit` Yank a commit-pinned URL. (no flags) Yank a branch-pinned URL. `:Forge review end` *:Forge-review-end* End the current review session. `:Forge review toggle` *:Forge-review-toggle* Toggle between unified and split view (|forge-review|). `:Forge clear` *:Forge-clear* Clear all internal caches (forge detection, repo info, list data). All subcommands support tab completion. ============================================================================== PICKERS *forge-pickers* *forge-picker* FORGE PICKER ~ The main entry point. Adapts based on the detected forge and current buffer. Lists: PRs/MRs, Issues, CI/CD, Browse Remote, Open File, Yank Commit URL, Yank Branch URL, Commits, Branches, Worktrees. Items that require a forge are hidden when no forge is detected. "Open File" and yank entries require a named buffer on a branch. *forge-picker-pr* PR PICKER ~ Lists PRs/MRs with number, title, author, and relative time. Action Default Key Description ~ `checkout` `` Check out the PR branch `diff` `` Start review (|forge-review|) `worktree` `` Create worktree from PR `ci` `` Open CI checks picker for this PR `browse` `` Open PR in browser `manage` `` Open management picker `create` `` Create new PR (|forge-compose|) `filter` `` Cycle state: open -> closed -> all `refresh` `` Clear cache and re-fetch *forge-picker-issue* ISSUE PICKER ~ Lists issues with number, title, author, and relative time. Action Default Key Description ~ `browse` `` Open issue in browser `close` `` Close or reopen the issue `filter` `` Cycle state: all -> open -> closed `refresh` `` Clear cache and re-fetch *forge-picker-ci* CI PICKER ~ Used for both per-PR checks (`:Forge pr ci {num}`) and repo-wide CI runs (`:Forge ci`). Both share the `keys.ci` bindings. Action Default Key Description ~ `log` `` View log (tail for running, failed-only for failures, full otherwise) `browse` `` Open run/check URL in browser `failed` `` Filter to failed `passed` `` Filter to passed `running` `` Filter to running `all` `` Show all `refresh` `` Re-fetch runs *forge-picker-commits* COMMITS PICKER ~ Git log with colored output and commit preview. Action Default Key Description ~ `checkout` `` Checkout commit (detached HEAD) `diff` `` Review the commit diff `browse` `` Open commit on forge `yank` `` Yank commit hash to `+` register *forge-picker-branches* BRANCHES PICKER ~ Uses fzf-lua's `git_branches` with additional actions. Action Default Key Description ~ `diff` `` Review diff against branch `browse` `` Open branch on forge *forge-picker-manage* MANAGE PICKER ~ Contextual actions for a specific PR, shown based on permissions and state: Approve, Merge (per available method: squash, rebase, merge), Close/Reopen, Mark as draft/Mark as ready. ============================================================================== REVIEW *forge-review* Review mode provides unified and split diff viewing for PRs and commits. Requires diffs.nvim for unified view and vim-fugitive for split view. Starting a review: ~ - `` on a PR in the PR picker - `` on a commit in the commit picker - `:Forge pr diff {num}` - `:Forge commit diff {sha}` - `:Forge branch diff {name}` Unified view (default): ~ diffs.nvim renders a combined diff in a `diffs://review:*` buffer with a quickfix list of changed files. Split view: ~ `:Forge review toggle` switches to a side-by-side fugitive split (`:Gvdiffsplit`). Run again to return to unified. Ending a review: ~ `:Forge review end` or wipe the `diffs://review:*` buffer. State: ~ `require('forge.review').state` holds: >lua { base = 'origin/main', mode = 'unified' } < `base` is `nil` when no review is active. ============================================================================== COMPOSE BUFFER *forge-compose* Creating a PR (without `--fill` or `--web`) opens a compose buffer at `forge://pr/new` with filetype `markdown` and buftype `acwrite`. Layout: ~ Line 1 PR title (pre-filled from single commit subject or branch) Line 2 Empty separator Lines 3+ PR body (from commit body or discovered PR template) `` End of metadata Write (`:w`) to push the branch and create the PR. An empty title or body aborts creation. The PR URL is copied to the `+` register on success. Metadata fields: ~ `Draft: yes` Create as draft (also `true`). Empty = not draft. `Reviewers:` Comma or space separated usernames. Template discovery: ~ forge.nvim searches forge-specific template paths. If a template directory contains multiple `.md` files, you are prompted to choose. GitHub: `.github/pull_request_template.md`, `.github/PULL_REQUEST_TEMPLATE/` GitLab: `.gitlab/merge_request_templates/` Codeberg: `.gitea/pull_request_template.md`, `.github/pull_request_template.md` Highlight groups: ~ Group Default Link Description ~ `ForgeComposeComment` `Comment` Comment block lines `ForgeComposeBranch` `Special` Branch names `ForgeComposeForge` `Type` Forge name `ForgeComposeDraft` `DiagnosticWarn` Draft status value `ForgeComposeFile` `Directory` File paths in diff stat `ForgeComposeAdded` `Added` Addition indicators (+) `ForgeComposeRemoved` `Removed` Deletion indicators (-) All groups are defined with `default = true`. ============================================================================== SOURCES *forge-sources* Built-in sources: ~ Name CLI Hosts matched ~ `github` `gh` `github` `gitlab` `glab` `gitlab` `codeberg` `tea` `codeberg`, `gitea`, `forgejo` Sources are lazy-loaded the first time a matching remote is detected. Built-in host patterns are checked after user-configured `sources` hosts, so user overrides take priority. HOST OVERRIDES ~ *forge-sources-hosts* To route a self-hosted instance to a built-in source: >lua vim.g.forge = { sources = { gitlab = { hosts = { 'gitlab.corp.com' } }, }, } < CUSTOM SOURCE REGISTRATION ~ *forge-sources-register* Register a custom source with `require('forge').register()`: >lua local my_source = { ... } require('forge').register('myforgejo', my_source) < The name must match the key used in `sources` host config. The source module is also discoverable as `forge.` (e.g. `require('forge.myforgejo')` if placed at `lua/forge/myforgejo.lua`). THE `forge.Forge` INTERFACE ~ *forge-Forge-interface* A source must implement all methods in the `forge.Forge` class. Each source is a table with these fields and methods: Fields: ~ `name` `string` Source name (e.g. `"github"`). `cli` `string` CLI executable name (e.g. `"gh"`). `kinds` `table` `{ issue = string, pr = string }` -- CLI subcommand names for issues and PRs. `labels` `table` `{ issue = string, pr = string,` `pr_one = string, pr_full = string,` `ci = string }` -- display labels. Required methods: ~ All methods receive `self` as the first argument (use `:` syntax). Method Returns ~ `list_pr_json_cmd(state)` `string[]` cmd to list PRs as JSON. `list_issue_json_cmd(state)` `string[]` cmd to list issues as JSON. `pr_json_fields()` `table` field name mapping: `{ number, title, branch, state,` `author, created_at }`. `issue_json_fields()` `table` field name mapping: `{ number, title, state, author,` `created_at }`. `view_web(kind, num)` Opens PR/issue in browser. `browse(loc, branch)` Opens file location on remote. `browse_root()` Opens repo root in browser. `browse_branch(branch)` Opens branch in browser. `browse_commit(sha)` Opens commit in browser. `checkout_cmd(num)` `string[]` cmd to checkout a PR. `yank_branch(loc)` Yanks branch-pinned URL. `yank_commit(loc)` Yanks commit-pinned URL. `fetch_pr(num)` `string[]` git fetch refspec for PR. `pr_base_cmd(num)` `string[]` cmd returning base branch. `pr_for_branch_cmd(branch)` `string[]` cmd returning PR number for a branch (empty = no PR). `checks_cmd(num)` `string` shell command for checks. `check_log_cmd(run_id, failed_only)` `string[]` cmd to view check log. `check_tail_cmd(run_id)` `string[]` cmd to tail check log. `list_runs_cmd(branch?)` `string` shell command for CI runs. `normalize_run(entry)` `forge.CIRun` normalized run entry. `run_log_cmd(id, failed_only)` `string[]` cmd to view run log. `run_tail_cmd(id)` `string[]` cmd to tail run log. `merge_cmd(num, method)` `string[]` cmd to merge PR. `approve_cmd(num)` `string[]` cmd to approve PR. `repo_info()` `forge.RepoInfo` with `permission` (`"ADMIN"`, `"WRITE"`, `"READ"`) and `merge_methods` (`string[]`). `pr_state(num)` `forge.PRState` with `state`, `mergeable`, `review_decision`, `is_draft`. `close_cmd(num)` `string[]` cmd to close PR. `reopen_cmd(num)` `string[]` cmd to reopen PR. `close_issue_cmd(num)` `string[]` cmd to close issue. `reopen_issue_cmd(num)` `string[]` cmd to reopen issue. `create_pr_cmd(title, body,` `string[]` cmd to create a PR. `base, draft, reviewers?)` `default_branch_cmd()` `string[]` cmd returning default branch name. `template_paths()` `string[]` repo-relative paths to PR templates. `draft_toggle_cmd(num, is_draft)` `string[]?` cmd to toggle draft. Return `nil` if unsupported. Optional methods: ~ Method Fallback ~ `list_runs_json_cmd(branch?)` `string[]` JSON CI list cmd. If absent, `list_runs_cmd` is used as a raw fzf-lua command source. `checks_json_cmd(num)` `string[]` JSON checks cmd. If absent, `checks_cmd` string is used as a raw fzf-lua command. `create_pr_web_cmd()` `string[]?` cmd to open web PR creation. Return `nil` to no-op. Skeleton: >lua local M = { name = 'myforgejo', 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', 'pulls', 'list', '--state', state, '--output', 'json' } end -- ... implement remaining methods ... return M < ============================================================================== HEALTH *forge-health* `:checkhealth forge` Reports on: ~ - `git` availability - Forge CLI availability (`gh`, `glab`, `tea`) - `fzf-lua` installation (required) - `diffs.nvim` installation (review mode) - Custom registered sources and their CLI availability ============================================================================== API *forge-api* PUBLIC API: `require('forge')` ~ *forge.detect()* `detect()` Returns `forge.Forge?`. Detects the forge for the current git repository by inspecting the `origin` remote URL. Returns `nil` if not in a git repo, no matching source, or the CLI is not installed. Results are cached per git root. *forge.config()* `config()` Returns `table`. The resolved configuration, merging `vim.g.forge` over defaults. If `vim.g.forge.keys` is `false`, `cfg.keys` is `false`. *forge.register()* `register(name, source)` Registers a custom `forge.Forge` source under `name`. *forge.registered_sources()* `registered_sources()` Returns `table`. All registered sources. *forge.create_pr()* `create_pr(opts?)` Creates a PR. `opts` is `forge.CreatePROpts?`: `draft` `boolean?` Create as draft. `instant` `boolean?` Skip compose buffer, fill from commits. `web` `boolean?` Push and open web create page. *forge.clear_cache()* `clear_cache()` Clears all internal caches (forge detection, repo info, git root, list data). *forge.repo_info()* `repo_info(f)` Returns `forge.RepoInfo`. Fetches and caches repository info (permissions, merge methods) for forge `f`. *forge.file_loc()* `file_loc()` Returns `string`. Current buffer file path relative to git root with line number(s). In visual mode, includes the selected line range (e.g. `"src/foo.lua:10-20"`). *forge.remote_web_url()* `remote_web_url()` Returns `string`. The HTTPS URL of the `origin` remote, normalized from SSH or git URLs. *forge.yank_url()* `yank_url(args)` Runs `args` (string[]) asynchronously and copies stdout to the `+` register. *forge.clear_list()* `clear_list(key?)` Clears cached list data. If `key` is given, clears only that key. If `nil`, clears all list caches. *forge.list_key()* `list_key(kind, state)` Returns `string`. Cache key for list data, scoped to git root. *forge.get_list()* `get_list(key)` Returns `table[]?`. Cached list data for `key`. *forge.set_list()* `set_list(key, data)` Stores `data` in the list cache under `key`. *forge.log()* `log(msg, level?)` Schedules a `vim.notify` with `[forge.nvim]:` prefix. *forge.log_now()* `log_now(msg, level?)` Synchronous `vim.notify` with `[forge.nvim]:` prefix and redraw. PUBLIC API: `require('forge.pickers')` ~ *forge.pickers.git()* `git()` Opens the main forge picker. Requires a git repository. *forge.pickers.pr()* `pr(state, f)` Opens the PR list picker. `state`: `"open"`, `"closed"`, or `"all"`. `f`: `forge.Forge`. *forge.pickers.issue()* `issue(state, f)` Opens the issue list picker. `state`: `"open"`, `"closed"`, or `"all"`. `f`: `forge.Forge`. *forge.pickers.checks()* `checks(f, num, filter?, cached_checks?)` Opens the checks picker for PR `num`. `filter`: `"all"`, `"fail"`, `"pass"`, or `"pending"`. *forge.pickers.ci()* `ci(f, branch?)` Opens the CI runs picker. `nil` branch shows all. *forge.pickers.commits()* `commits(f)` Opens the commit log picker. `f` may be `nil` (browse action requires a forge). *forge.pickers.branches()* `branches(f)` Opens the branch picker. `f` may be `nil`. *forge.pickers.pr_manage()* `pr_manage(f, num)` Opens the management picker for PR `num`. *forge.pickers.pr_actions()* `pr_actions(f, num)` Returns `table`. Action functions for PR `num`, keyed by fzf binding. Also has `_by_name` table keyed by action name (`"checkout"`, `"diff"`, `"worktree"`, `"browse"`, `"ci"`, `"manage"`). *forge.pickers.issue_close()* `issue_close(f, num)` Closes issue `num`. *forge.pickers.issue_reopen()* `issue_reopen(f, num)` Reopens issue `num`. PUBLIC API: `require('forge.review')` ~ *forge.review.start()* `start(base, mode?)` Starts a review session. `base` is the diff range (e.g. `"origin/main"`, `"abc123^..abc123"`). `mode`: `"unified"` (default) or `"split"`. *forge.review.stop()* `stop()` Ends the current review session. Clears state. *forge.review.toggle()* `toggle()` Toggles between unified and split view. No-op if no review is active. *forge.review.nav()* `nav(nav_cmd)` Returns `function`. Creates a navigation function for `nav_cmd` (`"cnext"`, `"cprev"`, `"lnext"`, `"lprev"`). In split mode, closes the split before navigating and reopens after. Wraps at list boundaries. *forge.review.state* `state` `table` with fields: `base` `string?` The current diff range, or `nil`. `mode` `"unified"|"split"` Current view mode. ============================================================================== vim:tw=78:ts=8:ft=help:norl: