blob: 2641ab494a4011852a34a56b01d308da16b47e3e [file] [log] [blame]
;; -*- lexical-binding: t -*-
;; Copyright 2020 The Chromium OS Authors. All rights reserved.
;; Use of this source code is governed by a BSD-style license that can be
;; found in the LICENSE file.
(require 'request)
(require 'xml)
(defcustom gerrit-repo-root
"The system path to the repo project root."
:type 'string)
(defcustom gerrit-git-cookies
(expand-file-name ".gitcookies" "~/")
"Path to gitcookies associated with your Gerrit account."
:type 'string)
(defcustom gerrit-hosts
;; TODO extract this variable from the
;; repo manifest instead of user configuration.
"Gerrit hosts you're interested in reviewing comments."
:type '(string))
(defcustom gerrit-curl-exec
"The curl-like executable for making requests to Gerrit.
Defaults to curl."
:type 'string)
(defconst gerrit--manifest-parser
(file-name-directory load-file-name))
"The executable used to parse the repo manifest as an alist.")
(defvar gerrit--change-to-host
(make-hash-table :test 'equal)
"Map showing => host.
Needed for generating links.")
(defun gerrit-init ()
"Initialize Repo Gerrit state."
;; If using cURL use special options.
;; Authenticate using cURL with gitcookies.
;; Use let to shadow dynamic scoped var to avoid
;; side effects with other users of request.el
(let ((request-curl gerrit-curl-exec)
(unless (string-suffix-p "gob-curl" gerrit-curl-exec)
;; Follow links and use git cookies.
`("-Lb" ,gerrit-git-cookies))))
(gerrit--init-global-repo-project-path-map gerrit--manifest-parser
(defvar gerrit--change-to-filepath-comments nil
"Map containing with change => filepath => comments.
filepath is from git project root, for the given change.")
(defvar gerrit--project-branch-pair-to-projectpath nil
"Map showing relative path from repo root to project.
Is of the form (project . dest-branch) => path-from-repo-root.")
(cl-defun gerrit--fetch-recent-changes (host &optional (count 3))
"Fetches recent changes as ChangeInfo entities.
host - Gerrit server address
count (optional) - the number of recent changes, default is 3
Fetch recent changes that are not abandoned/merged, and
thus are actionable, returns an array of hashtables that
represent Gerrit ChangeInfo entities."
(format "https://%s/a/changes/" host)
;; We don't use "status:reviewed" because that
;; only counts reviews after latest patch,
;; but we may want reviews before the latest patch too.
:params `(("q" . "owner:self status:open")
("n" . ,(format "%d" count)))
:sync t
:parser 'gerrit--request-response-json-parser)))
(defun gerrit--request-response-json-parser ()
"Response parsing callback for use with request.el
parses Gerrit response json payload by removing the
embedded XSS protection string before using a real json parser."
(json-parse-string (replace-regexp-in-string "^[[:space:]]*)]}'" "" (buffer-string))))
(defun gerrit--fetch-comments (host change)
"Gets recent comments for open Gerrit CLs.
Returns a map of the form path => sequence of comments,
where path is the filepath from the gerrit project root
and each comment represents a CommentInfo entity from Gerrit"
(format "https://%s/a/changes/%s~master~%s/comments"
(url-hexify-string (gethash "project" change))
(gethash "change_id" change))
:sync t
:parser 'gerrit--request-response-json-parser)))
(defun gerrit--fetch-change-to-file-to-comments ()
"Returns a map of maps of the form:
change => filepath => array(CommentInfo Map),
where filepath is from the nearest git root for a file.
Only fetches recent changes for open CLs."
(let ((out-map (make-hash-table :test 'equal)))
(loop for host in gerrit-hosts do
(loop for change across (gerrit--fetch-recent-changes host) do
(setf (gethash (gethash "change_id" change)
(setf (gethash change out-map)
(gerrit--fetch-comments host change))))
(defun gerrit--init-global-comment-map ()
"Inits `gerrit--change-to-filepath-comments`."
(setf gerrit--change-to-filepath-comments
(cl-defun gerrit--project-branch-pair-to-path-map (path-to-manifest-parser-exec
"Return map (project . dest-branch) => path-from-repo-root.
Parses the repo manifest using the given parser executable.
Assumes that stdout of parser is a Lisp alist of the form:
((project . dest-branch) . path-from-repo-root)."
(let (parsed-alist
(output (make-hash-table :test 'equal))
(tmp-buffer-name "*gerrit-temp--buffer*"))
(when (get-buffer tmp-buffer-name)
(kill-buffer tmp-buffer-name))
(unless (= 0 (call-process path-to-manifest-parser-exec
(message "Error parsing manifest investigate %s" tmp-buffer-name)
(cl-return-from gerrit--project-branch-pair-to-path-map nil))
(set-buffer tmp-buffer-name)
(goto-char (point-min))
(setf parsed-alist (read (current-buffer)))
(kill-buffer tmp-buffer-name))
(loop for item in parsed-alist do
(setf (gethash (car item) output) (cdr item)))
(defun gerrit--init-global-repo-project-path-map (path-to-manifest-parser-exec
"Initializes `gerrit--project-branch-pair-to-projectpath`.
This function is idempotent."
;; Here we use Python expat sax parser as it's considerably faster.
(unless gerrit--project-branch-pair-to-projectpath
(setf gerrit--project-branch-pair-to-projectpath
(defun gerrit--get-abs-path-to-file (filepath-from-project-git-root
"Returns full system path of the first argument.
`gerrit--project-branch-pair-to-projectpath` must be initialized."
(gethash (cons (gethash "project" project-branch-pair)
(gethash "branch" project-branch-pair))
(provide 'gerrit)
(require 'gerrit)