emacs: Gerrit Summary of Comments

The "gerrit-comments" commant presents a magit-like buffer that acts
as an interface to discover Gerrit comments and quickly navigate to
them on the user's system from their emacs.

BUG=chromium:1101373
TEST=Evaluated, initialized values, and used new interface.

Change-Id: I9bf0061eaeb9f157fe1a9015488733fa7adf5d30
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2320468
Reviewed-by: Jack Rosenthal <jrosenth@chromium.org>
Reviewed-by: Sean McAllister <smcallis@google.com>
Tested-by: Aaron Massey <aaronmassey@chromium.org>
Commit-Queue: Aaron Massey <aaronmassey@chromium.org>
diff --git a/contrib/emacs/gerrit-section.el b/contrib/emacs/gerrit-section.el
new file mode 100644
index 0000000..e881689
--- /dev/null
+++ b/contrib/emacs/gerrit-section.el
@@ -0,0 +1,211 @@
+;; -*- 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 'magit)
+(require 'magit-section)
+(require 'repo-gerrit)
+
+(defface gerrit-patch
+  `((t :inherit magit-section-heading))
+  "patch")
+
+
+(defface gerrit-filepath
+  `((t :inherit magit-branch-remote))
+  "filepath")
+
+
+(defconst gerrit-buffer-name "gerrit-comments"
+  "The name of the buffer where the gerrit summary is placed.")
+
+
+(defconst gerrit-section-mode-map magit-section-mode-map
+  "Mode map for Gerrit Summary - copies standard magit map")
+
+
+(defconst gerrit-section-type (gensym)
+  "Magit Sections need a symbol for a section type.
+We don't currently use this functionality.")
+
+
+(define-derived-mode gerrit-section-mode magit-section-mode
+  "Gerrit-Repo"
+  "Mode for displaying Gerrit comments within Emacs."
+  (when (fboundp 'evil-set-initial-state)
+    ;; Evil Mode doesn't always play nice with the keymaps.
+    (evil-set-initial-state 'gerrit-section-mode 'emacs)))
+
+
+
+(defun gerrit-refresh ()
+  "Refreshes Gerrit data from hosts."
+  (interactive)
+  (setf gerrit--refreshed t)
+  (gerrit-init)
+  (when (get-buffer gerrit-buffer-name)
+    (save-excursion
+      (switch-to-buffer gerrit-buffer-name)
+      (setf inhibit-read-only t)
+      (erase-buffer)
+      (setf inhibit-read-only t)))
+  (gerrit-comments t))
+
+
+(defun gerrit-comments (&optional refresh)
+  "Display buffer that shows comments for recent open changes.
+This comment is idempotent."
+  (interactive)
+  (when (or (not (get-buffer gerrit-buffer-name))
+            refresh)
+    (save-excursion
+      (set-buffer (get-buffer-create gerrit-buffer-name))
+      (magit-insert-section (root)
+        (magit-insert-heading "Gerrit Comments\n\n")
+        (loop for change in
+              (hash-table-keys gerrit--change-to-filepath-comments) do
+              (unless (hash-table-empty-p
+                       (gethash change gerrit--change-to-filepath-comments))
+                (magit-insert-section (file)
+                  (magit-insert-heading
+                    (format "%s - %s"
+                            (gethash "subject" change)
+                            (gethash "change_id" change)))
+
+                  (loop for filepath in
+                        (hash-table-keys
+                         (gethash
+                          change
+                          gerrit--change-to-filepath-comments))
+                        do
+                        (gerrit--insert-section-comments change
+                                                         filepath))))))
+
+      (when (hash-table-empty-p gerrit--change-to-filepath-comments)
+        (insert "No open changes!"))
+
+      (goto-char (point-min))
+      (gerrit-section-mode)
+      (setf word-wrap t)
+      (setf truncate-lines nil)))
+
+  (if refresh
+      (switch-to-buffer gerrit-buffer-name)
+    (pop-to-buffer gerrit-buffer-name)))
+
+
+(define-button-type 'gerrit--filepath
+  'face 'gerrit-filepath)
+
+(defun gerrit--navigate-to-comment (project-branch-pair
+                                    line
+                                    filepath-from-project-root
+                                    section-symbol)
+  "Navigates the user to a comment."
+
+  ;; Keep section open for user's to refer back to during edits.
+  (when (oref section-symbol hidden)
+    (magit-section-toggle section-symbol))
+
+  (switch-to-buffer-other-window
+   (find-file-noselect (gerrit--get-abs-path-to-file
+                        filepath-from-project-root
+                        project-branch-pair
+                        test-repo-root)))
+
+  (goto-char (point-min))
+  (beginning-of-line line))
+
+
+(defun gerrit--insert-comment-header (change
+                                      filepath-from-project-root
+                                      line
+                                      section-symbol)
+  "Inserts comments for the given change and system filepath."
+  (let (header-text
+        button-p
+        (begin-pos (point)))
+    (cond ((equal "/PATCHSET_LEVEL" filepath-from-project-root)
+           (setf header-text
+                 (propertize
+                  "Patch Comment"
+                  'face 'gerrit-filepath)))
+          ((equal "/COMMIT_MSG" filepath-from-project-root)
+           ;; TODO future CL for navigating
+           ;; to commit message lines.
+           (setf header-text
+                 (propertize
+                  (format "Commit Message:%s\n"
+                          filepath-from-project-root
+                          line)
+                  'face 'gerrit-filepath)))
+          ((equal "MERGE_LIST" filepath-from-project-root)
+           (message "There are merge list comments which we don't support"))
+          ;; Default here are filepath comments
+          (t (progn
+               (setf header-text
+                     (propertize
+                      (format "%s:%s\n"
+                              filepath-from-project-root
+                              line)
+                      'face 'gerrit-filepath))
+               (setf button-p t))))
+
+    (magit-insert-heading header-text)
+    (when button-p
+      (make-button begin-pos
+                   (point)
+                   'type 'gerrit--filepath
+                   'action
+                   (lambda (button)
+                     (gerrit--navigate-to-comment
+                      change
+                      line
+                      filepath-from-project-root
+                      section-symbol))))))
+
+
+(defun gerrit--insert-section-comments (change
+                                        filepath-from-project-root)
+  ;; We want the smaller lines first.
+  (sort (gethash filepath-from-project-root
+                 (gethash change gerrit--change-to-filepath-comments))
+        (lambda (a b)
+          ;; If comment has no line, we don't care about ordering.
+          (< (or (gethash "line" a) 0)
+             (or (gethash "line" b) 0))))
+
+  (loop for comment-info across
+        (gethash filepath-from-project-root
+                 (gethash change gerrit--change-to-filepath-comments))
+        do
+        ;; We don't care about our own comments.
+        (unless (equal
+                 test-user ;; FIXME when test code is removed
+                 (gethash "email" (gethash "author" comment-info)))
+          (let ((section-symbol (gensym))
+                (line (gethash "line" comment-info))
+                (begin-pos (point)))
+
+            (magit-insert-section
+              ;; We use section symbols to toggle when navigating.
+              section-symbol
+              (gerrit-section-type nil t)
+
+              (gerrit--insert-comment-header
+               change
+               filepath-from-project-root
+               line
+               section-symbol)
+              (gerrit--section-insert-comment comment-info))))))
+
+
+(defun gerrit--section-insert-comment (comment-info)
+  "Inserts the author, email, and body of a comment."
+  (magit-insert-section-body
+    (insert (format "Author: %s\nEmail: %s\nComment: %s\n"
+                    (gethash "name" (gethash "author" comment-info))
+                    (gethash "email" (gethash "author" comment-info))
+                    (gethash "message" comment-info)))))