2016-09-27 14:23:52 +00:00
|
|
|
;;; magithub-ci.el --- Show CI status as a magit-status header -*- lexical-binding: t; -*-
|
|
|
|
|
|
|
|
;; Copyright (C) 2016 Sean Allred
|
|
|
|
|
|
|
|
;; Author: Sean Allred <code@seanallred.com>
|
|
|
|
;; Keywords: tools
|
|
|
|
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
|
|
;; it under the terms of the GNU General Public License as published by
|
|
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
|
|
;; (at your option) any later version.
|
|
|
|
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
;; GNU General Public License for more details.
|
|
|
|
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
|
|
;; Provide the CI status of "origin" in the Magit status buffer.
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
(require 'magit)
|
|
|
|
(require 'magit-section)
|
|
|
|
(require 'magit-popup)
|
2016-10-12 08:42:37 +00:00
|
|
|
(require 'dash)
|
2016-09-27 14:23:52 +00:00
|
|
|
|
|
|
|
(require 'magithub-core)
|
|
|
|
(require 'magithub-cache)
|
|
|
|
|
2016-10-12 08:42:37 +00:00
|
|
|
(defconst magithub-ci-status-symbol-alist
|
|
|
|
'(("✔" . success)
|
|
|
|
("✖" . failure) ; also means `error'... gross
|
|
|
|
("●" . pending))
|
|
|
|
"Because hub 2.3 is silly and does silly things.
|
|
|
|
Reference: https://github.com/github/hub/blob/master/commands/ci_status.go#L107")
|
|
|
|
|
|
|
|
(defconst magithub-ci-status-regex
|
|
|
|
(rx bos
|
|
|
|
(group any) (* any) "\t"
|
|
|
|
(group (* any)) "\t"
|
|
|
|
(? (group (* any))) eos))
|
|
|
|
|
|
|
|
(defvar magithub-ci-urls nil
|
|
|
|
"An alist mapping of repositories to CI urls.")
|
|
|
|
|
2016-09-29 07:33:23 +00:00
|
|
|
(defun magithub-ci-enabled-p ()
|
|
|
|
"Non-nil if CI is enabled for this repository.
|
|
|
|
If magithub.ci.enabled is not set, CI is considered to be enabled."
|
|
|
|
(when (member (magit-get "magithub" "ci" "enabled") '(nil "yes")) t))
|
|
|
|
(defun magithub-ci-disable ()
|
|
|
|
"Disable CI for this repository."
|
|
|
|
(magit-set "no" "magithub" "ci" "enabled"))
|
|
|
|
(defun magithub-ci-enable ()
|
|
|
|
"Enable CI for this repository."
|
|
|
|
(magit-set "yes" "magithub" "ci" "enabled"))
|
|
|
|
|
2016-09-27 14:23:52 +00:00
|
|
|
(defun magithub-maybe-insert-ci-status-header ()
|
|
|
|
"If this is a GitHub repository, insert the CI status header."
|
2016-09-29 07:33:23 +00:00
|
|
|
(when (and (magithub-ci-enabled-p)
|
|
|
|
(magithub-usable-p))
|
2016-09-27 14:23:52 +00:00
|
|
|
(magithub-insert-ci-status-header)))
|
|
|
|
|
2016-09-29 07:33:23 +00:00
|
|
|
(defun magithub-ci-toggle ()
|
|
|
|
"Toggle CI integration."
|
|
|
|
(interactive)
|
|
|
|
(if (magithub-ci-enabled-p)
|
|
|
|
(magithub-ci-disable)
|
|
|
|
(magithub-ci-enable))
|
|
|
|
(when (derived-mode-p 'magit-status-mode)
|
|
|
|
(magit-refresh)))
|
|
|
|
|
|
|
|
(magit-define-popup-action 'magithub-dispatch-popup
|
|
|
|
?~ "Toggle CI for this repository" #'magithub-ci-toggle ?`)
|
|
|
|
|
2016-09-27 14:23:52 +00:00
|
|
|
(defun magithub-ci-status ()
|
|
|
|
"One of 'success, 'error, 'failure, 'pending, or 'no-status."
|
|
|
|
(let ((same-commit
|
|
|
|
(string-equal (magit-rev-parse "HEAD")
|
|
|
|
(magithub-ci-status-current-commit))))
|
|
|
|
(unless same-commit
|
2016-09-29 07:33:23 +00:00
|
|
|
(magithub-cache-clear (magithub-repo-id) :ci-status))
|
|
|
|
(if (eq (magithub-cache-value (magithub-repo-id) :ci-status)
|
|
|
|
'success)
|
2016-09-27 14:23:52 +00:00
|
|
|
'success
|
2016-09-29 07:33:23 +00:00
|
|
|
(magithub-cache (magithub-repo-id) :ci-status
|
2016-09-27 14:23:52 +00:00
|
|
|
'(magithub-ci-status--internal)))))
|
|
|
|
|
|
|
|
(defun magithub-ci-status-current-commit (&optional new-value)
|
|
|
|
"The commit our cached value corresponds to."
|
|
|
|
(let ((keys (list "magithub" "ci" "lastCommit")))
|
|
|
|
(if new-value (apply #'magit-set new-value keys)
|
|
|
|
(apply #'magit-get keys))))
|
|
|
|
|
2016-10-12 08:42:37 +00:00
|
|
|
(defun magithub-ci-update-urls (statuses)
|
|
|
|
"Updates `magithub-ci-urls' according to STATUSES.
|
|
|
|
See also `magithub-repo-id'."
|
|
|
|
(let* ((id (magithub-repo-id))
|
|
|
|
(repo-urls (assoc id magithub-ci-urls))
|
|
|
|
(urls (mapcar (lambda (s) (plist-get s :url)) statuses)))
|
|
|
|
(if repo-urls (setcdr repo-urls urls)
|
|
|
|
(add-to-list 'magithub-ci-urls (cons id urls))
|
|
|
|
urls)))
|
|
|
|
|
|
|
|
(defun magithub-ci-status--parse-2.2.8 (output)
|
|
|
|
"Backwards compatibility for old versions of hub.
|
|
|
|
See `magithub-ci-status--parse'."
|
|
|
|
(--when-let (cdr (s-match (rx bos (group (+ (any alpha space)))
|
|
|
|
(? ": " (group (+ (not (any " "))))) eos)
|
|
|
|
output))
|
|
|
|
(let ((status (list :status (intern (replace-regexp-in-string "\s" "-" (car it)))
|
|
|
|
:url (cadr it))))
|
|
|
|
(magithub-ci-update-urls (list status))
|
|
|
|
status)))
|
|
|
|
|
2016-09-29 07:33:23 +00:00
|
|
|
(defun magithub-ci-status--internal (&optional for-commit)
|
2016-09-27 14:23:52 +00:00
|
|
|
"One of 'success, 'error, 'failure, 'pending, or 'no-status."
|
2016-10-12 08:42:37 +00:00
|
|
|
(let* ((current-commit (magit-rev-parse "HEAD"))
|
|
|
|
(last-commit (or for-commit current-commit))
|
|
|
|
(output (magithub--command-output "ci-status" `("-v" ,last-commit))))
|
|
|
|
(--if-let (if (magithub-hub-version-at-least "2.3")
|
|
|
|
(car (magithub-ci-status--parse output))
|
|
|
|
(magithub-ci-status--parse-2.2.8 (car output)))
|
|
|
|
(prog1 (or (plist-get it :status) 'no-status)
|
|
|
|
(if (not (or for-commit (plist-get it :status)))
|
|
|
|
(let ((last-commit (magithub-ci-status--last-commit)))
|
|
|
|
(unless (string-equal current-commit last-commit)
|
|
|
|
(magithub-ci-status--internal last-commit))
|
|
|
|
(magithub-ci-status-current-commit current-commit))
|
|
|
|
(magithub-ci-status-current-commit current-commit)))
|
|
|
|
(beep)
|
|
|
|
(setq magithub-hub-error
|
|
|
|
(message
|
|
|
|
(concat "Hub didn't have any recognizable output for \"ci-status\"!\n"
|
|
|
|
"Are you connected to the internet?\n"
|
|
|
|
"Consider submitting an issue to github/hub.")))
|
|
|
|
'internal-error)))
|
|
|
|
|
|
|
|
(defun magithub-ci-status--parse (output)
|
|
|
|
"Parse a string OUTPUT into a list of statuses.
|
|
|
|
The first status will be an `overall' status."
|
|
|
|
(let ((statuses (mapcar #'magithub-ci-status--parse-line output))
|
|
|
|
(get-status (lambda (status) (lambda (s) (eq (plist-get s :status) status)))))
|
|
|
|
(magithub-ci-update-urls statuses)
|
|
|
|
(cons
|
|
|
|
(list :check 'overall
|
|
|
|
:status
|
|
|
|
(cond
|
|
|
|
((-all? (funcall get-status 'success) statuses) 'success)
|
|
|
|
((-some? (funcall get-status 'pending) statuses) 'pending)
|
|
|
|
((-some? (funcall get-status 'error) statuses) 'error)
|
|
|
|
((-some? (funcall get-status 'failure) statuses) 'failure)))
|
|
|
|
statuses)))
|
|
|
|
|
|
|
|
(defun magithub-ci-status--parse-line (line)
|
|
|
|
"Parse a single LINE of status into a status plist."
|
|
|
|
(--if-let (cdr (s-match magithub-ci-status-regex line))
|
|
|
|
(list :status (cdr (assoc (car it) magithub-ci-status-symbol-alist))
|
|
|
|
:url (car (cddr it))
|
|
|
|
:check (cadr it))
|
|
|
|
(if (string= line "no-status")
|
|
|
|
'no-status
|
|
|
|
(if (string= line "") 'no-output))))
|
2016-09-27 14:23:52 +00:00
|
|
|
|
|
|
|
(defun magithub-ci-status--last-commit ()
|
|
|
|
"Find the commit considered to have the current CI status.
|
|
|
|
Right now, this finds the most recent commit without
|
|
|
|
|
|
|
|
[ci skip]
|
|
|
|
|
|
|
|
or
|
|
|
|
|
|
|
|
[skip ci]
|
|
|
|
|
|
|
|
in the commit message.
|
|
|
|
|
|
|
|
See the following resources:
|
|
|
|
|
|
|
|
- https://docs.travis-ci.com/user/customizing-the-build#Skipping-a-build
|
|
|
|
- https://circleci.com/docs/skip-a-build/"
|
|
|
|
(let* ((args '("--invert-grep"
|
|
|
|
"--grep=\\[ci skip\\]"
|
|
|
|
"--grep=\\[skip ci\\]"
|
|
|
|
"--format=oneline"
|
|
|
|
"--max-count=1"))
|
|
|
|
(output (magit-git-lines "log" args)))
|
|
|
|
(car (split-string (car output)))))
|
|
|
|
|
|
|
|
(defvar magithub-ci-status-alist
|
|
|
|
'((no-status . "None")
|
|
|
|
(error . "Error")
|
|
|
|
(internal-error . magithub-ci--hub-error-string)
|
|
|
|
(failure . "Failure")
|
|
|
|
(pending . "Pending")
|
|
|
|
(success . "Success")))
|
|
|
|
|
|
|
|
(defun magithub-ci--hub-error-string ()
|
|
|
|
"Internal error string."
|
|
|
|
(format "Internal error!\n%s" magithub-hub-error))
|
|
|
|
|
|
|
|
(defface magithub-ci-no-status
|
|
|
|
'((((class color)) :inherit magit-dimmed))
|
|
|
|
"Face used when CI status is `no-status'."
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defface magithub-ci-error
|
|
|
|
'((((class color)) :inherit magit-signature-untrusted))
|
|
|
|
"Face used when CI status is `error'."
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defface magithub-ci-pending
|
|
|
|
'((((class color)) :inherit magit-signature-untrusted))
|
|
|
|
"Face used when CI status is `pending'."
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defface magithub-ci-success
|
|
|
|
'((((class color)) :inherit magit-signature-good))
|
|
|
|
"Face used when CI status is `success'."
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defface magithub-ci-failure
|
|
|
|
'((((class color)) :inherit magit-signature-bad))
|
|
|
|
"Face used when CI status is `'"
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defface magithub-ci-unknown
|
|
|
|
'((((class color)) :inherit magit-signature-untrusted))
|
|
|
|
"Face used when CI status is `unknown'."
|
|
|
|
:group 'magithub-faces)
|
|
|
|
|
|
|
|
(defun magithub-ci-visit ()
|
|
|
|
"Browse the CI.
|
|
|
|
Sets up magithub.ci.url if necessary."
|
|
|
|
(interactive)
|
2016-10-12 08:42:37 +00:00
|
|
|
(let* ((urls (cdr (assoc (magithub-repo-id) magithub-ci-urls)))
|
|
|
|
(target-url (if (= 1 (length urls)) (car urls)
|
|
|
|
(when urls (completing-read "CI Dashboard URL: " urls)))))
|
|
|
|
(when (or (null target-url) (string= "" target-url))
|
|
|
|
(user-error "No CI URL detected"))
|
|
|
|
(browse-url target-url)))
|
2016-09-27 14:23:52 +00:00
|
|
|
|
|
|
|
(defvar magit-magithub-ci-status-section-map
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
(define-key map [remap magit-visit-thing] #'magithub-ci-visit)
|
|
|
|
(define-key map [remap magit-refresh] #'magithub-ci-refresh)
|
|
|
|
map)
|
|
|
|
"Keymap for `magithub-ci-status' header section.")
|
|
|
|
|
|
|
|
(defun magithub-ci-refresh ()
|
|
|
|
"Invalidate the CI cache and refresh the buffer."
|
|
|
|
(interactive)
|
2016-09-29 07:33:23 +00:00
|
|
|
(magithub-cache-clear (magithub-repo-id) :ci-status)
|
2016-09-27 14:23:52 +00:00
|
|
|
(when (derived-mode-p 'magit-status-mode)
|
|
|
|
(magit-refresh)))
|
|
|
|
|
|
|
|
(defun magithub-insert-ci-status-header ()
|
|
|
|
(let* ((status (magithub-ci-status))
|
|
|
|
(face (intern (format "magithub-ci-%s"
|
|
|
|
(symbol-name status))))
|
|
|
|
(status-val (cdr (assq status magithub-ci-status-alist))))
|
|
|
|
(magit-insert-section (magithub-ci-status)
|
|
|
|
(insert (format "%-10s" "CI: "))
|
|
|
|
(insert (propertize
|
|
|
|
(cond
|
|
|
|
((stringp status-val) status-val)
|
|
|
|
((functionp status-val) (funcall status-val))
|
|
|
|
(t (format "%S" status-val)))
|
|
|
|
'face (if (facep face) face 'magithub-ci-unknown)))
|
|
|
|
(insert ?\n))))
|
|
|
|
|
2016-10-12 08:42:37 +00:00
|
|
|
(magithub--deftoggle magithub-toggle-ci-status-header
|
|
|
|
magit-status-headers-hook #'magithub-maybe-insert-ci-status-header "the CI header")
|
2016-09-27 14:23:52 +00:00
|
|
|
|
2016-10-12 08:42:37 +00:00
|
|
|
(when (executable-find magithub-hub-executable)
|
|
|
|
(magithub-toggle-ci-status-header))
|
2016-09-27 14:23:52 +00:00
|
|
|
|
|
|
|
(provide 'magithub-ci)
|
|
|
|
;;; magithub-ci.el ends here
|