my-emacs-d/elpa/helm-github-stars-20150625.1523/helm-github-stars.el
2016-10-14 12:23:22 +02:00

425 lines
16 KiB
EmacsLisp

;;; helm-github-stars.el --- Helm integration for your starred repositories on github -*- lexical-binding: t; -*-
;;
;; Author: Sliim <sliim@mailoo.org>
;; xuchunyang <xuchunyang56@gmail.com>
;; URL: https://github.com/Sliim/helm-github-stars
;; Package-Version: 20150625.1523
;; Version: 1.3.2
;; Package-Requires: ((helm "1.6.8") (emacs "24.4"))
;; Keywords: helm github stars
;; This file is not part of GNU Emacs.
;;; License:
;; 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 GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;; helm-github-stars provides capabilities to fetch your starred
;; repositories from github and select one for browsing.
;;
;; Install:
;;
;; M-x package-install helm-github-stars
;;
;; Usage:
;;
;; Copy helm-github-stars.el in your load-path and put this in your ~/.emacs.d/init.el:
;;
;; (require 'helm-github-stars)
;; ;; Setup your github username:
;; (setq helm-github-stars-username "USERNAME")
;;
;; Type M-x helm-github-stars to show starred repositories.
;;
;; At the first execution of helm-github-stars, list of repositories is
;; fetched from github and saved into a cache file.
;; Default cache location: $HOME/.emacs.d/hgs-cache.
;; To refresh cache and open helm interface run helm-github-stars-fetch.
;;
;; You can customize cache file path:
;;
;; (setq helm-github-stars-cache-file "/cache/path")
;;
;; For a clean look, repositories's description is aligned by default, you can
;; customize this behavior via helm-github-stars-name-length, it's default
;; value is 30.
;; You can disable this by setting helm-github-stars-name-length to nil:
;;
;; (setq helm-github-stars-name-length nil)
;;
;; If you want to be able to show your private repositories, customize
;; helm-github-stars-token:
;;
;; (setq helm-github-stars-token "TOKEN")
;;; Code:
;; requires
(require 'helm)
(require 'helm-utils)
(require 'json)
(require 'subr-x)
(defgroup helm-github-stars nil
"Helm integration for your starred repositories on github."
:group 'helm
:prefix "helm-github-stars-")
(defcustom helm-github-stars-username "Sliim"
"Github's username to fetch starred repositories."
:type 'string)
(defcustom helm-github-stars-token nil
"Access token to use for your repositories and your starred repositories.
If you don't have or don't want to show your private repositories, you don't
need access token at all.
To generate an access token:
1. Visit the page https://github.com/settings/tokens/new and
login to github (if asked).
2. Give the token any name you want (helm-github-stars, for instance).
3. The permission we need is \"repo\" and \"delete_repo\", so unmark
all others.
4. Click on \"Generate Token\", copy the generated token, and
save it to this variable by writing
(setq helm-github-stars-token TOKEN)
somewhere in your configuration and evaluating it (or just
restart emacs).
DISCLAIMER
When you save this variable, DON'T WRITE IT ANYWHERE PUBLIC. This
token grants (very) limited access to your account.
END DISCLAIMER
when disabled (nil) don't use Github token."
:type '(choice (string :tag "Token")
(const :tag "Disable" nil)))
(defcustom helm-github-stars-cache-file (concat user-emacs-directory "hgs-cache")
"Cache file for starred repositories."
:type 'file)
(defcustom helm-github-stars-name-length 30
"Length of repo name before truncate.
When disabled (nil) don't align description."
:type '(choice (const :tag "Disabled" nil)
(integer :tag "Length before truncate")))
(defcustom helm-github-stars-refetch-time nil
"Days to refetch cache file.
When disabled (nil) don't refetch automatically. "
:type '(choice (const :tag "Disabled" nil)
(number :tag "Days to refetch cache file")))
(defcustom helm-github-stars-full-frame helm-full-frame
"Use full-screen window to show candidates in the `helm-github-stars' command.
Its default value is `helm-full-frame' (whose default value is nil)."
:type 'boolean)
(defcustom helm-github-stars-default-sources '(hgs/helm-c-source-stars
hgs/helm-c-source-repos
hgs/helm-c-source-search)
"Default sources list used in the `helm-github-stars' command."
:type '(repeat (choice symbol)))
(defvar hgs/github-url "https://github.com/"
"Github URL for browsing.")
(defvar helm-github-stars-clone-done-hook nil
"Normal hook run by `hgs/clone', after clone is done.
Function in this hook takes one argument, path to just cloned repo.
For example, to open just cloned repo in dired automatically:
(add-hook 'helm-github-stars-clone-done-hook #'dired)")
(defvar hgs/helm-stars-common-actions
'(("Browse URL" . (lambda (_candidate)
(dolist (candidate (helm-marked-candidates))
(browse-url
(concat hgs/github-url candidate)))))
("Show URL" . (lambda (candidate)
(message
(concat hgs/github-url candidate))))
("Clone" . hgs/clone)))
(defvar hgs/helm-stars-actions
(append hgs/helm-stars-common-actions
'(("Unstar" . (lambda (candidate)
"Unstar a starred repository."
(hgs/unstar-or-delete-repo
"https://api.github.com/user/starred/"
candidate))))))
(defvar hgs/helm-repos-actions
(append hgs/helm-stars-common-actions
'(("Delete" . (lambda (candidate)
"Delete a user repository."
(hgs/unstar-or-delete-repo
"https://api.github.com/repos/"
candidate))))))
(defvar hgs/helm-c-source-stars
(helm-build-in-buffer-source "Starred repositories"
:init (lambda ()
(with-current-buffer (helm-candidate-buffer 'local)
(insert (mapconcat 'identity (hgs/get-github-stars) "\n"))))
:real-to-display (lambda (candidate) (hgs/align-description candidate))
:coerce (lambda (candidate) (hgs/get-repo-name candidate))
:action hgs/helm-stars-actions)
"Helm source definition.")
(defvar hgs/helm-c-source-repos
(helm-build-in-buffer-source "Your repositories"
:init (lambda ()
(with-current-buffer (helm-candidate-buffer 'local)
(insert (mapconcat 'identity (hgs/get-github-repos) "\n"))))
:real-to-display (lambda (candidate) (hgs/align-description candidate))
:coerce (lambda (candidate) (hgs/get-repo-name candidate))
:action hgs/helm-repos-actions)
"Helm source definition.")
(defvar hgs/helm-c-source-search
(helm-build-dummy-source "Search on github"
:action (lambda (candidate)
(browse-url (concat "https://github.com/search?q=" candidate)))))
(defun hgs/align-description (item)
"Truncate repo name in ITEM."
(let* ((index (string-match " - " item))
(name (substring item 0 index))
(description (substring item (+ 3 index))))
(concat
;; Name
(if (> (string-width name) helm-github-stars-name-length)
(helm-substring-by-width name helm-github-stars-name-length)
(concat name (make-string
(- (+ helm-github-stars-name-length 3)
(string-width name)) ? )))
;; Separator
" "
;; Description
description)))
(defun hgs/read-cache-file ()
"Read cache file and return list of starred repositories."
(with-temp-buffer
(insert-file-contents helm-github-stars-cache-file)
(read (current-buffer))))
(defun hgs/write-cache-file (hash)
"Write HASH of repositories in cache file."
(with-temp-buffer
(let ((file helm-github-stars-cache-file)
(coding-system-for-write 'utf-8))
(print hash (current-buffer))
(when (file-writable-p file)
(write-region (point-min) (point-max) file)))))
(defun hgs/cache-file-exists ()
"Check that cache file exists."
(file-exists-p helm-github-stars-cache-file))
(defun hgs/clear-cache-file ()
"Delete file cache if exists."
(when (hgs/cache-file-exists)
(delete-file helm-github-stars-cache-file)))
(defun hgs/clear-cache-file-by-time ()
"Delete cache file if it is too old."
(when (and (hgs/cache-file-exists) helm-github-stars-refetch-time)
(let ((time-since-last-fetch
(time-subtract (current-time)
(nth 5 (file-attributes helm-github-stars-cache-file)))))
(when (> (time-to-number-of-days time-since-last-fetch)
helm-github-stars-refetch-time)
(hgs/clear-cache-file)))))
(defun hgs/generate-cache-file ()
"Generate or regenerate cache file if already exists."
(let ((stars-list [])
(repos-list [])
(cache-hash-table (make-hash-table :test 'equal)))
;; Fetch user's starred repositories
(let ((next-request t)
(current-page 1))
(while next-request
(let ((response (hgs/parse-github-response
(funcall (if helm-github-stars-token
'hgs/request-github-stars-by-token
'hgs/request-github-stars)
current-page))))
(if (= 0 (length response))
(setq next-request nil)
(progn
(setq stars-list (vconcat stars-list response))
(setq current-page (1+ current-page)))))))
;; Fetch user's repositories
(let ((next-request t)
(current-page 1))
(while next-request
(let ((response (hgs/parse-github-response
(funcall (if helm-github-stars-token
'hgs/request-github-repos-by-token
'hgs/request-github-repos)
current-page))))
(if (= 0 (length response))
(setq next-request nil)
(progn
(setq repos-list (vconcat repos-list response))
(setq current-page (1+ current-page)))))))
(puthash '"stars" stars-list cache-hash-table)
(puthash '"repos" repos-list cache-hash-table)
(hgs/write-cache-file cache-hash-table)))
(defun hgs/request-github-stars (page)
"Request Github API user's stars with PAGE parameter and return response."
(hgs/request-github (concat "https://api.github.com/users/"
helm-github-stars-username
"/starred?per_page=100&page="
(number-to-string page))))
(defun hgs/request-github-repos (page)
"Request Github API user's repositories with PAGE parameter and return response."
(hgs/request-github (concat "https://api.github.com/users/"
helm-github-stars-username
"/repos?per_page=100&page="
(number-to-string page))))
(defun hgs/request-github-repos-by-token (page)
"Request Github API user's repositories and return response."
(let ((url-request-extra-headers `(("Authorization" .
,(format "token %s" helm-github-stars-token)))))
(hgs/request-github (concat
"https://api.github.com/user/repos?per_page=100&page="
(number-to-string page)))))
(defun hgs/request-github-stars-by-token (page)
"Request Github API user's repositories and return response."
(let ((url-request-extra-headers `(("Authorization" .
,(format "token %s" helm-github-stars-token)))))
(hgs/request-github (concat
"https://api.github.com/user/starred?per_page=100&page="
(number-to-string page)))))
(defun hgs/request-github (url)
"Request Github URL and return response."
(with-current-buffer
(url-retrieve-synchronously url)
(let ((start (save-excursion
(goto-char (point-min))
(and (re-search-forward "\\[" (point-max) t)
(match-beginning 0)))))
(and start
(decode-coding-string
(buffer-substring-no-properties start (point-max))
'utf-8)))))
(defun hgs/parse-github-response (response)
"Parse Github API RESPONSE to get repositories full name."
(let ((github-repos (json-read-from-string response))
(repos [])
(i 0))
(while (< i (length github-repos))
(setq repos (vconcat repos (vector (concat
(cdr (assoc 'full_name (elt github-repos i)))
" - "
(cdr (assoc 'description (elt github-repos i)))))))
(setq i (1+ i)))
repos))
(defun hgs/get-github-stars ()
"Get user's starred repositories."
(when (not (hgs/cache-file-exists))
(hgs/generate-cache-file))
(gethash "stars" (hgs/read-cache-file)))
(defun hgs/get-github-repos ()
"Get user's repositories."
(when (not (hgs/cache-file-exists))
(hgs/generate-cache-file))
(gethash "repos" (hgs/read-cache-file)))
(defun hgs/unstar-or-delete-repo (api repo-name)
"Unstar a starred repository or delete a user repository."
(unless helm-github-stars-token
(error "`helm-github-stars-token' is nil."))
(let ((url-request-method "DELETE")
(url-request-extra-headers
`(("Authorization" . ,(format "token %s" helm-github-stars-token)))))
(with-current-buffer (url-retrieve-synchronously (concat api repo-name))
(goto-char (point-min))
(when (not (string-match "204 No Content" (buffer-string)))
(error "Problem unstar or delete repo."))
(kill-buffer)))
;; FIXME: there is no way to modify the cache file
;; (puthash "stars" (delete candidate (gethash "stars" (hgs/read-cache-file)))
;; (hgs/read-cache-file))
;; (hgs/write-cache-file cache-hash-table)
(helm-github-stars-fetch))
(defun hgs/get-repo-name (candidate)
(substring candidate 0 (string-match " - " candidate)))
(defun hgs/clone (candidate)
"Git clone in the background..
When git cloen is not yet done, use `list-processes' to dispaly related process."
(unless (executable-find "git") (error "git not found"))
(let* ((repo-name candidate)
(repository (format "https://github.com/%s.git" repo-name))
(default-clone-directory
(substring repo-name (1+ (string-match "/" repo-name))))
(directory
(read-directory-name
(format "Clone %s to: " repo-name) nil nil nil default-clone-directory))
(command (format "git clone %s %s" repository directory))
(output-buffer "*git-clone-output*")
(proc (start-process-shell-command "git-clone" output-buffer command)))
(set-process-sentinel
proc
(lambda (_process event)
(unless (string-equal event "finished\n")
(error "Git clone failed, see %s buffer for details" output-buffer))
(kill-buffer output-buffer)
(message "Git clone done.")
(run-hook-with-args 'helm-github-stars-clone-done-hook directory)))))
(defun helm-github-stars-fetch ()
"Remove cache file before calling helm-github-stars."
(interactive)
(hgs/clear-cache-file)
(helm-github-stars))
;;;###autoload
(defun helm-github-stars ()
"Show and Browse your github's stars."
(interactive)
(hgs/clear-cache-file-by-time)
(helm :sources helm-github-stars-default-sources
:candidate-number-limit 9999
:buffer "*helm github stars*"
:prompt "> "
:full-frame helm-github-stars-full-frame))
(provide 'helm-github-stars)
;;; helm-github-stars.el ends here