Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gerrit.nvim

A Neovim plugin for Gerrit Code Review. Browse changes, read the diff, leave inline comments, vote, and post the review without opening the web UI.

It talks to Gerrit over ssh on port 29418. There is no token to generate and nothing to configure: if git push to your Gerrit works, so does this.

Picking a change, commenting on a line, voting and publishing the review

Requirements

  • Neovim 0.10 or newer
  • ssh and git
  • A Gerrit server you can reach over ssh (2.12 or newer, for gerrit review --json)
  • Optional: telescope.nvim for the change picker; without it vim.ui.select is used

Install

With lazy.nvim:

{ 'SafaeOuajih/gerrit.nvim', cmd = 'Gerrit', opts = {} }

Use

Everything hangs off one command.

Command What it does
:Gerrit pick from the open changes of the current project
:Gerrit dashboard what is waiting for you, across every project
:Gerrit status:merged owner:self pick from any Gerrit query
:Gerrit mine your own open changes
:Gerrit open 12345 the overview of one change
:Gerrit diff 12345 its diff, ready to be commented on
:Gerrit review [12345] compose and publish a review
:Gerrit message 12345 lgtm post a one-line message
:Gerrit drafts the comments you have not published yet
:Gerrit watch [on|off] be told when a review starts waiting for you

Where a change number is optional, it is taken from the current buffer when you are already looking at a change.

The dashboard

:Gerrit dashboard is the browser tab you kept open: the changes waiting on your review, your own open changes, and what you have landed lately, in one buffer.

Gerrit dashboard  gerrit.example.com

Waiting for you (3)
  4242     +2  foo: stop leaking the parser context     Alice          1h ago
  4118     -1  net: handle a truncated header           Bo Lindqvist   1d ago
  4301         docs: typo                               Chandra        5m ago

Your changes (1)
  4300         baz: rework the retry loop               Safae Ouajih   2h ago

Recently merged (1)
  4290         build: bump the toolchain                Safae Ouajih   4d ago
Key Action
<CR> open the change
d open its diff
R review it
gx open it in a browser
r run every section again
q close

The sections are plain Gerrit queries, so they are yours to change:

dashboard = {
  sections = {
    { title = 'Needs my vote', query = 'status:open reviewer:self NOT owner:self NOT label:Code-Review=2' },
    { title = 'Blocked',       query = 'status:open owner:self label:Code-Review=-1' },
    { title = 'My topic',      query = 'status:open topic:the-big-refactor' },
  },
}

Two things to know about them. They are sent exactly as written -- unlike :Gerrit, they are neither scoped to the current project nor narrowed by max_age, because a review waiting on you is waiting on you wherever you have a shell open. And write NOT owner:self, not the -owner:self you would type in the web UI: ssh hands the query over as plain arguments, and a word starting with a dash is taken for a command line option long before Gerrit's parser sees it.

Being told, rather than going to look

:Gerrit watch polls the "waiting for you" query in the background and notifies you about what has changed since the last look:

Alice is waiting for your review on 4242  foo: stop leaking the parser context

Nothing is announced twice. Which change was announced, and how recently it had been touched, is remembered in a small JSON file under stdpath('state'), so restarting Neovim does not replay your inbox at you; a reply on a change you already knew about comes through as new activity on 4242, and several at once arrive as a single notification rather than a stack of them.

Have it running from the start, and pick your own definition of "waiting":

watch = {
  enabled  = true,
  query    = 'status:open reviewer:self NOT owner:self',
  interval = 300, -- seconds
}

On Gerrit 3.3 and newer, attention:self is sharper than reviewer:self: it is the same attention set the web UI's bold rows come from, so a change you have already reviewed drops out of it until the ball is back in your court.

For a statusline:

require('gerrit').status()  -- "Gerrit 3", or "" when nothing is waiting
require('gerrit').count()   -- 3

It is off by default: a plugin should not start talking to a server on a timer just because it was installed. :checkhealth gerrit runs the watch query once when it is on, since a query the server rejects would otherwise fail where nobody can see it.

The overview

:Gerrit opens a picker; choosing a change shows its subject, owner, branch, current votes, file list, commit message and the discussion so far.

Key Action
<CR> open the diff
R review this change
gx open it in a browser
r refresh
q close

The diff

The patch set is fetched with git fetch and shown as a unified diff. Every line knows which file and line number it maps to on Gerrit's side, so a comment lands where you put the cursor.

Key Action
c comment on the cursor line, or on the visual selection
X drop the draft comment under the cursor
]c / [c jump between comments
R review this change
gf open the whole file at this line
q close

You do not have to be in the change's repository, or in any repository at all. refs/changes/... lives on the Gerrit server rather than on your disk, so when the change belongs to a project you have not cloned -- the usual case coming out of the dashboard -- the patch set is fetched into a bare mirror under mirror_dir instead, two commits deep and nothing more. Being in the project's own checkout is still better, and is still preferred when it is what you have: the objects are already there, and gf then opens the real, editable file rather than the committed one.

Commenting on a diff --git header attaches the comment to the file as a whole. Commenting on a removed line attaches it to the parent side, which is what Gerrit does when you comment on the left-hand pane.

Comments already on the server are shown inline in grey; your unpublished drafts are shown in the warning colour with a marker in the sign column.

The review

R opens a buffer holding the labels the change uses and room for a message:

# Change 4242  foo: stop leaking the parser context
# tools/thing  main  patch set 2
#
# 2 draft comments will be published with this review:
#   src/foo.c:12  off by one here
#   src/foo.c:40  why was this dropped?
#
# Vote by writing a value after a label; an empty value leaves it alone.
# Everything under the labels is the message. Lines starting with # are dropped.
# <C-s> publishes, q aborts.

Code-Review: +1
Verified:

Two nits inline, otherwise this looks right to me.

<C-s> (or :w) sends the message, the votes and every draft comment as a single Gerrit review. q throws the buffer away and keeps the drafts.

Drafts survive quitting Neovim they are mirrored to a small JSON file under stdpath('state') and are dropped only once the review is published.

Configuration

Defaults, all optional:

require('gerrit').setup {
  -- The server. Both are read from the git remote when left nil, which is
  -- what makes the plugin work across several Gerrit servers unconfigured.
  -- Set `host` to an ~/.ssh/config alias and leave `port` nil to let ssh
  -- supply the port.
  host = nil,
  port = nil,
  ssh_args = { '-o', 'BatchMode=yes' },

  remote = nil,   -- git remote to fetch patch sets from; nil tries gerrit, review, origin
  project = nil,  -- project name; nil reads it from the remote URL

  query = 'status:open',   -- the default query for `:Gerrit`
  scope_to_project = true, -- narrow that query to the current project
  max_age = '2w',          -- and to the changes touched that recently; nil for all of them
  limit = 100,             -- most changes a query will return

  labels = { 'Code-Review', 'Verified' }, -- offered when the change lists none
  unresolved = true,                      -- publish comments as needing a reply

  timeout = 30000, -- ms before an ssh or git call is abandoned
  browser = nil,   -- command for the browse key; nil uses vim.ui.open
  draft_file = vim.fs.joinpath(vim.fn.stdpath 'state', 'gerrit-drafts.json'),

  -- Where patch sets of projects you have not cloned are fetched to.
  mirror_dir = vim.fs.joinpath(vim.fn.stdpath 'cache', 'gerrit.nvim'),

  -- Sent as written: not scoped to the project, not narrowed by max_age.
  dashboard = {
    sections = {
      { title = 'Waiting for you', query = 'status:open reviewer:self NOT owner:self' },
      { title = 'Your changes',    query = 'status:open owner:self' },
      { title = 'Recently merged', query = 'status:merged owner:self limit:10' },
    },
  },

  watch = {
    enabled = false, -- poll for reviews waiting on you, in the background
    query = 'status:open reviewer:self NOT owner:self',
    interval = 300,  -- seconds; anything under 60 is rounded up to it
    state_file = vim.fs.joinpath(vim.fn.stdpath 'state', 'gerrit-watch.json'),
  },

  keys = {
    change = { diff = '<CR>', review = 'R', browse = 'gx', refresh = 'r', quit = 'q' },
    diff = {
      comment = 'c',
      delete = 'X',
      next = ']c',
      prev = '[c',
      review = 'R',
      edit = 'gf',
      quit = 'q',
    },
    review = { publish = '<C-s>', quit = 'q' },
    dashboard = { open = '<CR>', diff = 'd', review = 'R', browse = 'gx', refresh = 'r', quit = 'q' },
  },
}

Set any key to false to leave it unmapped. The options that are lists -- ssh_args, labels, dashboard.sections -- replace the defaults rather than merging with them, so three sections in means three sections out.

How far back it looks

status:open on a project that has been running for years matches every change anybody ever left hanging, and the server walks all of them before the first row reaches us. :Gerrit and :Gerrit mine therefore ask only for the changes somebody has touched in the last max_age, which shows up in the picker title:

status:open NOT age:2w

Any Gerrit age will do: 36h, 2w, 3mon, 1y. Set max_age = nil for the old, exhaustive listing. A query you write yourself is never narrowed, so :Gerrit status:open is the one-off way to see everything, and :Gerrit open 12345 opens a change however long it has been sitting there.

Several servers

Nothing needs to be configured per project. The ssh destination and the project name come from the git remote of the repository the current buffer lives in, so one install covers every Gerrit you work with. host is only needed when your remote is an HTTPS URL, since an HTTPS remote says nothing about where ssh should connect.

Troubleshooting

:checkhealth gerrit reports which server was worked out, from which remote, and whether it answers. The three things it usually catches:

  • the host does not resolve you are off the VPN
  • ssh asks for a passphrase your key is not in the agent (ssh-add -l)
  • no remote looks like Gerrit set host in setup()

How it works

  • gerrit query --format=JSON lists and describes changes
  • git fetch <remote> refs/changes/NN/CHANGE/PATCHSET brings the patch set local
  • git diff-tree --root -p renders it
  • gerrit review --json <change>,<patchset> publishes the message, the votes and the inline comments as one Gerrit ReviewInput on stdin

Sending the review as JSON on stdin is what makes inline comments possible over ssh, and it sidesteps having to quote a multi-line message onto a command line.

About

Review Gerrit changes from Neovim over SSH browse changes, read diffs, leave inline comments, vote, publish.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages