Move from magit-gh-pulls to magithub

This commit is contained in:
Gergely Polonkai 2016-09-27 16:23:52 +02:00
parent 0c2e649db0
commit fbf952b215
10 changed files with 828 additions and 636 deletions

View File

@ -1,27 +0,0 @@
;;; magit-gh-pulls-autoloads.el --- automatically extracted autoloads
;;
;;; Code:
(add-to-list 'load-path (or (file-name-directory #$) (car load-path)))
;;;### (autoloads nil "magit-gh-pulls" "magit-gh-pulls.el" (22387
;;;;;; 29358 559405 127000))
;;; Generated autoloads from magit-gh-pulls.el
(autoload 'magit-gh-pulls-mode "magit-gh-pulls" "\
Pull requests support for Magit
\(fn &optional ARG)" t nil)
(autoload 'turn-on-magit-gh-pulls "magit-gh-pulls" "\
Unconditionally turn on `magit-pulls-mode'.
\(fn)" nil nil)
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; End:
;;; magit-gh-pulls-autoloads.el ends here

View File

@ -1 +0,0 @@
(define-package "magit-gh-pulls" "20160513.310" "GitHub pull requests extension for Magit" '((emacs "24") (gh "0.9.1") (magit "2.1.0") (pcache "0.2.3") (s "1.6.1")) :url "https://github.com/sigma/magit-gh-pulls" :keywords '("git" "tools"))

View File

@ -1,608 +0,0 @@
;;; magit-gh-pulls.el --- GitHub pull requests extension for Magit
;; Copyright (C) 2011-2015 Yann Hodique, Alexander Yakushev
;; Author: Yann Hodique <yann.hodique@gmail.com>
;; Keywords: git tools
;; Package-Version: 20160513.310
;; Version: 0.5.2
;; URL: https://github.com/sigma/magit-gh-pulls
;; Package-Requires: ((emacs "24") (gh "0.9.1") (magit "2.1.0") (pcache "0.2.3") (s "1.6.1"))
;; This file 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 2, or (at your option)
;; any later version.
;; This file 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., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; This is a Magit extension for manipulating GitHub pull requests
;; No configuration is needed in the repository if any of your remotes contain a
;; URL to Github's remote repository. If for some reason you don't have any
;; Github remotes in your config, you can specify username and repository
;; explicitly:
;; $ git config magit.gh-pulls-repo <user>/<repo> # your github repository
;; Add these lines to your init.el:
;; (require 'magit-gh-pulls)
;; (add-hook 'magit-mode-hook 'turn-on-magit-gh-pulls)
;; These are the bindings for pull requests, defined in magit-gh-pulls-mode-map:
;; # g --- refreshes the list of pull requests
;; # f --- fetches the commits associated with the pull request at point
;; # b --- helps you creating a topic branch from a review request
;; # m --- merges the PR on top of the current branch
;; # c --- creates a PR from the current branch
;; # o --- opens a pull request on GitHub in your default browser
;; Then, you can do whatever you want with the commit objects associated with
;; the pull request (merge, cherry-pick, diff, ...)
;; When you create a new pull request, you can enable -w option to automatically
;; open it on GitHub in your default browser.
;;; Code:
(require 'eieio)
(require 'magit)
(require 'git-commit)
(require 'gh)
(require 'gh-pulls)
(require 'pcache)
(require 's)
(require 'cl-lib)
(defgroup magit-gh-pulls nil
"Github.com pull-requests for Magit."
:group 'magit-extensions)
(defcustom magit-gh-pulls-open-new-pr-in-browser nil
"DEPRECATED: use magit switch instead."
:group 'magit-gh-pulls
:type 'boolean)
(defvar magit-gh-pulls-maybe-filter-pulls 'identity
"Filter function which should validate pulls you want to be
viewed in magit. It receives a list of pull requests and should
return a list of pull requests.")
(defvar magit-gh-pulls-collapse-commits t
"Collapse commits in pull requests listing.")
(defvar magit-gh-pulls-pull-detail-limit 10
"Pull in additional information for each pull request in the
status buffer only if the total number of open PRs is <=
this number. Additional information includes individual
commits in each PR and highlighting based on the merge
status of the PR. Increasing this number may adversely
affect performance on repos with many PRs.")
(defvar-local magit-gh-pulls-previous-winconf nil)
(defvar magit-gh-pulls-editor-mode-map
(let ((map (make-keymap)))
(define-key map (kbd "C-c C-c") 'magit-gh-pulls-pull-editor-finish)
(define-key map (kbd "C-c C-k") 'magit-gh-pulls-pull-editor-quit)
map))
(define-derived-mode magit-gh-pulls-editor-mode text-mode "Magit GitHub Pulls Editor"
(font-lock-add-keywords nil (git-commit-mode-font-lock-keywords) t))
(easy-menu-define magit-gh-pulls-editor-mode-menu magit-gh-pulls-editor-mode-map
"Magit GitHub Pulls Editor Menu"
'("Magit GitHub Pulls"
["Submit Pull Request" magit-gh-pulls-pull-editor-finish t]
["Cancel" magit-gh-pulls-pull-editor-quit t]))
(defun magit-gh-pulls-get-api ()
(gh-pulls-api "api" :sync t :num-retries 1 :cache (gh-cache "cache")))
(defun magit-gh-pulls-get-repo-from-config ()
"Return (user . project) pair read from magit.gh-pulls-repo
config option."
(let* ((cfg (magit-get "magit" "gh-pulls-repo")))
(when cfg
(let* ((split (split-string cfg "/")))
(cons (car split) (cadr split))))))
;;Find all the Hostname Lines until we hit the end of config-lines or the
;;next Host line. Return '(remaining-config-lines list-of-hostnames)
(defun magit-gh-pulls-collect-hostnames (config-lines)
(let ((cur-line (car config-lines))
(rest config-lines)
(result '()))
(while (and cur-line (not (string= (cadr cur-line) "Host")))
(setq result (cons (cadr (cdr cur-line)) result))
(setq rest (cdr rest))
(setq cur-line (car rest)))
(list rest result)))
(defun magit-gh-pulls-get-host-hostnames (config-lines)
(let (result-alist
(curline (car config-lines))
(rest-lines (cdr config-lines)))
(while rest-lines
(if (string= (cadr curline) "Host")
(let ((hosts (s-split "\\s*" (cadr (cdr curline)))) ;;List of the host aliases
(rest-result (magit-gh-pulls-collect-hostnames rest-lines)))
(dolist (host hosts)
;;Host must be lowercase because the url parser lowercases the string
(setq result-alist (cons (cons (downcase host) (cadr rest-result)) result-alist)))
(setq curline (caar rest-result))
(setq rest-lines (cdar rest-result)))
(progn
(setq curline (car rest-lines))
(setq rest-lines (cdr rest-lines)))))
result-alist))
(defun -magit-gh-pulls-filter-and-split-host-lines (lines)
(delq nil
(mapcar (lambda (line)
(s-match "^[ \t]*\\(Host\\|HostName\\|Hostname\\)[ \t]+\\(.+\\)$" line))
lines)))
;; Port of github/hub's SSHConfig
(defun magit-gh-pulls-get-ssh-config-hosts ()
(let* ((file-lines (mapcar (lambda (path)
(if (file-exists-p path)
(with-temp-buffer
(insert-file-contents path)
(split-string (buffer-string) "\n" t))
'()))
(list
(concat (file-name-as-directory (getenv "HOME")) ".ssh/config")
"/etc/ssh_config"
"/etc/ssh/ssh_config")))
(all-lines (apply #'append file-lines))
(matched-lines (-magit-gh-pulls-filter-and-split-host-lines all-lines)))
(magit-gh-pulls-get-host-hostnames matched-lines)))
;; Port of github/hub's ParseURL, with modifications to align with existing parse-url
(defun magit-gh-pulls-parse-url (url ssh-config-hosts)
(let* ((fixed-url (if (and (not (s-matches? "^[a-zA-Z_-]+://" url))
(s-matches? ":" url)
(not (s-matches? "\\\\\\\\" url))) ;;Two literal backlashes
(concat "ssh://" (s-replace ":" "/" url))
url))
(parsed-url (url-generic-parse-url fixed-url))
(ssh-host (when (string= (url-type parsed-url) "ssh")
(assoc (url-host parsed-url) ssh-config-hosts))))
(when (and ssh-host (cadr ssh-host))
(setf (url-host parsed-url) (cadr ssh-host)))
(when (and
(string= (url-host parsed-url) "github.com")
(s-matches? "\\(git\\|ssh\\|https?\\)" (url-type parsed-url)))
(let ((creds (s-match "/\\(.+\\)/\\([^/]+\\)/?$" (url-filename parsed-url))))
(when creds
(cons (cadr creds) (s-chop-suffix ".git" (cadr (cdr creds)))))))))
(defun magit-gh-pulls-guess-repo-from-origin ()
"Return (user . project) pair inferred from remotes in
.git/config."
(let ((creds nil)
(ssh-config-hosts (magit-gh-pulls-get-ssh-config-hosts)))
(dolist (remote (magit-git-lines "remote") creds)
(let ((parsed (magit-gh-pulls-parse-url
(magit-get "remote" remote "url")
ssh-config-hosts)))
(when parsed
(setq creds parsed))))))
(defun magit-gh-pulls-guess-repo ()
"Return (user . project) pair obtained either from explicit
option, or inferred from remotes."
(or (magit-gh-pulls-get-repo-from-config)
(magit-gh-pulls-guess-repo-from-origin)))
(defun magit-gh-pulls-requests-cached-p (api user proj)
"Returns T if the API request to the given USER and PROJ is cached."
(let ((cache-repo (format "/repos/%s/%s/pulls" user proj))
(cached? nil))
(pcache-map (oref api :cache)
(lambda (key _) (when (equal (car key) cache-repo)
(setq cached? t))))
cached?))
(defun magit-gh-pulls-insert-gh-pulls ()
(condition-case-unless-debug print-section
(progn
(let* ((repo (magit-gh-pulls-guess-repo)))
(when repo
(let* ((api (magit-gh-pulls-get-api))
(user (car repo))
(proj (cdr repo))
(cached? (magit-gh-pulls-requests-cached-p api user proj))
(stubs (when cached?
(funcall magit-gh-pulls-maybe-filter-pulls
(oref (gh-pulls-list api user proj) :data))))
(num-total-stubs (length stubs))
(i 0)
(branch (magit-get-current-branch)))
(when (or (> (length stubs) 0) (not cached?))
(magit-insert-section (pulls)
(magit-insert-heading "Pull Requests:")
(dolist (stub stubs)
(cl-incf i)
(let* ((id (oref stub :number))
(base-sha (oref (oref stub :base) :sha))
(base-ref (oref (oref stub :base) :ref))
(head-sha (oref (oref stub :head) :sha))
;; branch has been deleted in the meantime...
(invalid (equal (oref (oref stub :head) :ref) head-sha))
(have-commits
(and (>= magit-gh-pulls-pull-detail-limit i)
(eql 0 (magit-git-exit-code "cat-file" "-e" base-sha))
(eql 0 (magit-git-exit-code "cat-file" "-e" head-sha))))
(applied (and have-commits
(magit-git-string "branch" branch
(format "--contains=%s" head-sha))))
(heading
(format "[%s@%s] %s\n"
(propertize (number-to-string id)
'face 'magit-tag)
(if (string= base-ref branch)
(propertize base-ref
'face 'magit-branch-local)
base-ref)
(propertize
(oref stub :title) 'face
(cond (applied 'magit-cherry-equivalent)
(have-commits nil)
(invalid 'error)
(t 'italic)))))
(info (list user proj id)))
(cond
(have-commits
(magit-insert-section
(pull info magit-gh-pulls-collapse-commits)
(insert heading)
(magit-insert-heading)
(when (and have-commits (not applied))
(magit-git-wash
(apply-partially 'magit-log-wash-log 'cherry)
"cherry" "-v" (magit-abbrev-arg)
base-sha head-sha))))
(invalid
(magit-insert-section (invalid-pull info)
(insert heading)))
(t
(magit-insert-section (unfetched-pull info)
(insert heading))))))
(when (not cached?)
(insert "Press `# g` to update the pull request list.\n\n"))
(when (> (length stubs) 0)
(insert "\n"))))))))
(error nil)))
(defun magit-gh-pulls-guess-topic-name (req)
(let ((user (oref (oref req :user) :login))
(topic (oref (oref req :head) :ref)))
(format "%s/%s" user topic)))
(defun magit-gh-section-req-data (&optional section)
(oref (apply #'gh-pulls-get
(magit-gh-pulls-get-api)
(magit-section-value (or section (magit-current-section))))
:data))
(defun magit-gh-pulls-diff-pull-request ()
(interactive)
(magit-section-case
(pull
(let* ((req (magit-gh-section-req-data))
(inhibit-magit-refresh t))
(magit-diff (concat (oref (oref req :base) :sha) ".."
(oref (oref req :head) :sha))))
(magit-refresh))
(unfetched-pull
(error "Please fetch pull request commits first"))
(invalid-pull
(error "This pull request refers to invalid reference"))))
(defun magit-gh-pulls-create-branch ()
(interactive)
(magit-section-case
(pull
(let* ((req (magit-gh-section-req-data))
(branch (read-from-minibuffer
"Branch name: " (magit-gh-pulls-guess-topic-name req)))
(base (magit-read-branch-or-commit
"Branch base: "
(oref (oref req :base) :ref)))
(inhibit-magit-refresh t))
(magit-branch-and-checkout branch base)
(magit-merge (oref (oref req :head) :sha)))
(magit-refresh))
(unfetched-pull
(error "Please fetch pull request commits first"))
(invalid-pull
(error "This pull request refers to invalid reference"))))
(defun magit-gh-pulls-merge-pull-request ()
(interactive)
(magit-section-case
(pull
(let* ((req (magit-gh-section-req-data))
(branch (magit-gh-pulls-guess-topic-name req))
(base (oref (oref req :base) :ref))
(inhibit-magit-refresh t))
(magit-branch-and-checkout branch base)
(magit-merge (oref (oref req :head) :sha))
(magit-checkout base)
(magit-merge branch (when (member "--no-ff" (magit-gh-pulls-arguments))
'("--no-ff")))
(magit-call-git "branch" "-D" branch))
(magit-refresh))
(unfetched-pull
(error "Please fetch pull request commits first"))
(invalid-pull
(error "This pull request refers to invalid reference"))))
(defun magit-gh-pulls-fetch-commits ()
(interactive)
(magit-section-case
(unfetched-pull
(let* ((req (magit-gh-section-req-data))
(head (oref req :head)))
(magit-run-git "fetch" (oref (oref head :repo) :ssh-url)
(oref head :ref))))
(pull nil)
(invalid-pull
(error "This pull request refers to invalid reference"))))
(defun magit-gh-pulls-url-for-pull (info)
"Return github url for a pull request using INFO."
(let ((url "https://github.com/%s/%s/pull/%s"))
(apply 'format url info)))
(defun magit-gh-pulls-open-in-browser ()
(interactive)
(let ((info (magit-section-value (magit-current-section))))
(magit-section-case
(pull (browse-url (magit-gh-pulls-url-for-pull info)))
(unfetched-pull (browse-url (magit-gh-pulls-url-for-pull info))))))
(defun magit-gh-pulls-purge-cache ()
(let* ((api (magit-gh-pulls-get-api))
(cache (oref api :cache))
(repo (magit-gh-pulls-guess-repo)))
(pcache-map cache (lambda (k v)
(when (string-match
(format "/repos/%s/%s/" (car repo) (cdr repo))
(car k))
(pcache-invalidate cache k))))))
(defun magit-gh-pulls-get-remote-default (&optional remote-name-override)
(let ((remote-name (or remote-name-override "origin"))
(remote-branches (magit-git-lines "branch" "-r"))
remote-head)
(while (and remote-branches (not remote-head))
(let ((m (s-match (format "^\\s-*%s/HEAD -> %s/\\(\\w*\\)" remote-name remote-name) (car remote-branches))))
(if m
(setq remote-head (cadr m))
(setq remote-branches (cdr remote-branches)))))
remote-head))
(defun magit-gh-pulls-build-req (api user proj callback)
"Builds a request entity for a new pull request. Under
synchronous flow (editor disabled), fires CALLBACK with
API, USER, PROJ and the new REQUEST as args. Under
asynchronous flow, passes all ARGS through to the PR
editor which is responsible for continuing the flow."
(let* ((current (magit-get-current-branch))
(current-default (magit-gh-pulls-get-remote-default))
(base-branch (magit-read-branch-or-commit "Base" current-default))
(head-branch (magit-read-branch-or-commit "Head" current)))
(let* ((head-remote (concat (magit-get-remote base-branch) "/" head-branch))
(pushed-p (and (magit-branch-p head-remote)
(null (magit-git-lines "diff" (concat head-remote ".." head-branch))))))
(when (and (not pushed-p)
(yes-or-no-p "PR branch doesn't appear to be pushed. Push it?"))
(magit-push current (magit-get-remote base-branch))))
(let* ((base
(make-instance 'gh-repos-ref :user (make-instance 'gh-users-user :name user)
:repo (make-instance 'gh-repos-repo :name proj)
:ref base-branch))
(head
(make-instance 'gh-repos-ref :user (make-instance 'gh-users-user :name user)
:repo (make-instance 'gh-repos-repo :name proj)
:ref head-branch))
(default-title (magit-git-string "log"
(format "%s..%s" base-branch head-branch)
"--format=%s" "--reverse"))
(default-body (mapconcat 'identity (magit-git-items "log"
(format "%s..%s" base-branch head-branch)
"--reverse" "--format=**%s**%n%b") "\n")))
(if (member "--use-pr-editor" (magit-gh-pulls-arguments))
(magit-gh-pulls-init-pull-editor api user proj default-title default-body base head callback)
(let* ((title (read-string "Title: " default-title))
(body (read-string "Description: " default-body))
(req (make-instance 'gh-pulls-request :head head :base base :body body :title title)))
(funcall callback api user proj req))))))
(defun magit-gh-pulls-pr-template-file ()
"Returns the path to the PULL_REQUEST_TEMPLATE file in the
current repository. Returns nil if there is not a pull request
template file."
(car (directory-files (magit-toplevel) t "^PULL_REQUEST_TEMPLATE")))
(defun magit-gh-pulls-init-pull-editor (api user proj default-title default-body base head callback)
"Create a new buffer for editing this pull request and
switch to it. The context needed to finalize the
pull request is stored in a buffer-local var in the
newly created buffer."
(let ((winconf (current-window-configuration))
(buffer (get-buffer-create (format "*magit-gh-pulls: %s*" proj)))
(context (make-hash-table :test 'equal)))
(dolist (var '(api user proj base head callback))
(puthash (symbol-name var) (eval var) context))
(split-window-vertically)
(other-window 1)
(switch-to-buffer buffer)
(funcall 'magit-gh-pulls-editor-mode)
(if (magit-gh-pulls-pr-template-file)
(progn (insert (or default-title "") "\n\n")
(insert-file-contents (magit-gh-pulls-pr-template-file)))
(insert (or default-title "") "\n\n" default-body))
(goto-char (point-min))
(message "Opening pull request editor. C-c C-c to finish, C-c C-k to quit.")
(setq-local magit-gh-pulls-editor-context context)
(setq magit-gh-pulls-previous-winconf winconf)))
(defun magit-gh-pulls-pull-editor-finish ()
"Finish editing the current pull request and continue
to submit it. This should be called interactively
from within a pull request editor buffer."
(interactive)
(if (eq nil magit-gh-pulls-editor-context)
(message "This function can only be run in a pull editor buffer.")
(let* ((context magit-gh-pulls-editor-context)
(end-of-first-line (save-excursion
(beginning-of-buffer)
(line-end-position)))
(title (s-trim (buffer-substring-no-properties 1 end-of-first-line)))
(body (s-trim (buffer-substring-no-properties end-of-first-line (point-max))))
(req (make-instance 'gh-pulls-request
:head (gethash "head" context)
:base (gethash "base" context)
:body body :title title)))
(funcall (gethash "callback" context)
(gethash "api" context)
(gethash "user" context)
(gethash "proj" context)
req)
(magit-gh-pulls-pull-editor-quit))))
(defun magit-gh-pulls-pull-editor-quit ()
"Cleanup the current pull request editor and restore
the previous window config."
(interactive)
(if (eq nil magit-gh-pulls-editor-context)
(message "This function can only be run in a pull editor buffer.")
(let ((winconf magit-gh-pulls-previous-winconf))
(kill-buffer)
(kill-local-variable 'magit-gh-pulls-previous-winconf)
(when winconf
(set-window-configuration winconf)))))
(defun magit-gh-pulls-create-pull-request ()
"Entrypoint for creating a new pull request."
(interactive)
(let ((repo (magit-gh-pulls-guess-repo)))
(when repo
(let* ((current-branch (magit-get-current-branch))
(api (magit-gh-pulls-get-api))
(user (car repo))
(proj (cdr repo)))
(magit-gh-pulls-build-req api user proj 'magit-gh-pulls-submit-pull-request)))))
(defun magit-gh-pulls-submit-pull-request (api user proj req)
"Endpoint for creating a new pull request. Sync and async
flows should both call this function to finish creating
a new pull request."
(interactive)
(let* ((a (gh-pulls-new api user proj req)))
(if (not (= (oref a :http-status) 201))
(message "Error creating pull-request: %s. Have you pushed the branch to github?" (cdr (assoc "Status" (oref a :headers))))
(let ((url (oref (oref a :data) :html-url)))
(message (concat "Created pull-request and copied URL to kill ring: " url))
(when (member "--open-new-in-browser" (magit-gh-pulls-arguments))
(browse-url url))
(kill-new url)))))
(defun magit-gh-pulls-reload ()
(interactive)
(let ((creds (magit-gh-pulls-guess-repo)))
(if (not (and creds (car creds) (cdr creds)))
(message "Remote repository is not configured or incorrect.")
(magit-gh-pulls-purge-cache)
(gh-pulls-list (magit-gh-pulls-get-api) (car creds) (cdr creds))
(magit-refresh))))
(easy-menu-define magit-gh-pulls-extension-menu
nil
"GitHub Pull Requests extension menu"
'("GitHub Pull Requests"
:visible magit-gh-pulls-mode
["Reload pull request" magit-gh-pulls-reload]
["Create pull request branch" magit-gh-pulls-create-branch]
["Fetch pull request commits" magit-gh-pulls-fetch-commits]
["Open pull request in browser" magit-gh-pulls-open-in-browser]
))
(easy-menu-add-item 'magit-mode-menu
'("Extensions")
magit-gh-pulls-extension-menu)
(magit-define-section-jumper magit-jump-to-pulls "Pull Requests" pulls)
(define-key magit-status-mode-map (kbd "jq") 'magit-jump-to-pulls)
(defvar magit-gh-pulls-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "#") 'magit-gh-pulls-popup)
map))
(defvar magit-gh-pulls-mode-lighter " Pulls")
;;;###autoload
(define-minor-mode magit-gh-pulls-mode "Pull requests support for Magit"
:lighter magit-gh-pulls-mode-lighter
:require 'magit-gh-pulls
:keymap 'magit-gh-pulls-mode-map
(or (derived-mode-p 'magit-mode)
(error "This mode only makes sense with magit"))
(if magit-gh-pulls-mode
(magit-add-section-hook
'magit-status-sections-hook
'magit-gh-pulls-insert-gh-pulls
'magit-insert-stashes)
(remove-hook 'magit-status-sections-hook 'magit-gh-pulls-insert-gh-pulls))
(when (called-interactively-p 'any)
(magit-refresh)))
;;;###autoload
(defun turn-on-magit-gh-pulls ()
"Unconditionally turn on `magit-pulls-mode'."
(magit-gh-pulls-mode 1))
(magit-define-popup magit-gh-pulls-popup
"Show popup buffer featuring Github Pull Requests commands."
'magit-commands
:switches '((?c "Produce merge commit" "--no-ff")
(?w "Open new PR in browser" "--open-new-in-browser")
(?e "Edit PR in full buffer" "--use-pr-editor"))
:actions '((?g "Reload" magit-gh-pulls-reload)
(?f "Fetch" magit-gh-pulls-fetch-commits)
(?d "Diff" magit-gh-pulls-diff-pull-request)
(?b "Make branch" magit-gh-pulls-create-branch)
(?m "Merge" magit-gh-pulls-merge-pull-request)
(?c "Create new PR" magit-gh-pulls-create-pull-request)
(?o "Open in browser" magit-gh-pulls-open-in-browser))
:default-action 'magit-gh-pulls-reload)
(provide 'magit-gh-pulls)
;; Local Variables:
;; indent-tabs-mode: nil
;; End:
;;; magit-gh-pulls.el ends here

View File

@ -0,0 +1,17 @@
;;; magithub-autoloads.el --- automatically extracted autoloads
;;
;;; Code:
(add-to-list 'load-path (directory-file-name (or (file-name-directory #$) (car load-path))))
;;;### (autoloads nil nil ("magithub-cache.el" "magithub-ci.el" "magithub-core.el"
;;;;;; "magithub-issue.el" "magithub-pkg.el" "magithub.el") (22506
;;;;;; 26931 120335 726000))
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; End:
;;; magithub-autoloads.el ends here

View File

@ -0,0 +1,67 @@
;;; magithub-cache.el --- caching network data -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
;; Keywords: data, 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:
;;
;;; Code:
(defvar magithub-cache--cache (make-hash-table)
"A hash table to use as a cache.
Entries are of the form (time-entered . value).")
(defvar magithub-cache-refresh-seconds-plist
(list :issues 600 :ci-status 60)
"The number of seconds that have to pass for GitHub data to be
considered outdated.")
(defun magithub-cache (cache default)
"The cached value for CACHE (set to DEFAULT if necessary)."
(declare (indent defun))
(let ((cached-value (gethash cache magithub-cache--cache :no-value)))
(if (or (eq cached-value :no-value)
(< (plist-get magithub-cache-refresh-seconds-plist cache)
(time-to-seconds (time-since (car cached-value)))))
(cdr (puthash cache (cons (current-time) (eval default))
magithub-cache--cache))
(when magithub-debug-mode
(message "Using cached value for %S (retrieved %s)"
cache (format-time-string "%F %T" (car cached-value))))
(cdr cached-value))))
(defun magithub-cache-value (cache)
"The cached value for CACHE."
(let ((c (gethash cache magithub-cache--cache :no-value)))
(unless (eq c :no-value)
(cdr c))))
(defun magithub-cache-clear (cache)
"Clear the cache."
(remhash cache magithub-cache--cache))
(defun magithub-refresh ()
"Refresh all GitHub data."
(interactive)
(setq magithub-cache--cache (make-hash-table))
(magit-refresh))
(provide 'magithub-cache)
;;; magithub-cache.el ends here

View File

@ -0,0 +1,197 @@
;;; 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)
(require 'magithub-core)
(require 'magithub-cache)
(defun magithub-maybe-insert-ci-status-header ()
"If this is a GitHub repository, insert the CI status header."
(when (and (magithub-github-repository-p)
(executable-find magithub-hub-executable))
(magithub-insert-ci-status-header)))
(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
(magithub-cache-clear :ci-status))
(if (eq (magithub-cache-value :ci-status) 'success)
'success
(magithub-cache :ci-status
'(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))))
(defun magithub-ci-status--internal (&optional ignore-ci-skips)
"One of 'success, 'error, 'failure, 'pending, or 'no-status."
(with-temp-message "Updating CI status..."
(let* ((last-commit (when ignore-ci-skips (magithub-ci-status--last-commit)))
(output (car (magithub--command-output "ci-status" last-commit))))
(if output
(let* ((output (replace-regexp-in-string "\s" "-" output))
(status (intern output)))
(if (and (not ignore-ci-skips) (eq status 'no-status))
(magithub-ci-status--internal t)
(magithub-ci-status-current-commit (magit-rev-parse "HEAD"))
status))
(beep)
(setq magithub-hub-error
(message
(concat "Hub didn't have any output for \"ci-status\"!\n"
"Consider submitting an issue to github/hub.")))
'internal-error))))
(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)
(let ((var-value (magit-get "magithub" "ci" "url")))
(unless var-value
(magit-set
(setq var-value (read-from-minibuffer "I don't know the CI URL yet -- what is it? I'll remember: ")
var-value (if (string-equal "" var-value) nil var-value))
"magithub" "ci" "url"))
(browse-url var-value)))
(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)
(magithub-cache-clear :ci-status)
(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))))
(defun magithub-toggle-ci-status-header ()
(interactive)
(if (memq #'magithub-maybe-insert-ci-status-header magit-status-headers-hook)
(remove-hook 'magit-status-headers-hook #'magithub-maybe-insert-ci-status-header)
(if (executable-find magithub-hub-executable)
(add-hook 'magit-status-headers-hook #'magithub-maybe-insert-ci-status-header t)
(message "Magithub: (magithub-toggle-ci-status-header) `hub' isn't installed, so I can't insert the CI header")))
(when (derived-mode-p 'magit-status-mode)
(magit-refresh)))
(magithub-toggle-ci-status-header)
(provide 'magithub-ci)
;;; magithub-ci.el ends here

View File

@ -0,0 +1,149 @@
;;; magithub-core.el --- core functions for magithub -*- 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:
;;
;;; Code:
(require 'magit)
(defun magithub-github-repository-p ()
"Non-nil if \"origin\" points to GitHub."
(let ((url (magit-get "remote" "origin" "url")))
(or (s-prefix? "git@github.com:" url)
(s-prefix? "https://github.com/" url)
(s-prefix? "git://github.com/" url))))
(defun magithub--api-available-p ()
"Non-nil if the API is available."
(let ((magit-git-executable "ping")
(magit-pre-call-git-hook nil)
(magit-git-global-arguments nil))
(= 0 (magit-git-exit-code "-c 1" "api.github.com"))))
(defun magithub--completing-read-multiple (prompt collection)
"Using PROMPT, get a list of elements in COLLECTION.
This function continues until all candidates have been entered or
until the user enters a value of \"\". Duplicate entries are not
allowed."
(let (label-list this-label done)
(while (not done)
(setq collection (remove this-label collection)
this-label "")
(when collection
;; @todo it would be nice to detect whether or not we are
;; allowed to create labels -- if not, we can require-match
(setq this-label (completing-read prompt collection)))
(unless (setq done (s-blank? this-label))
(push this-label label-list)))
label-list))
(defconst magithub-hash-regexp
(rx bow (= 40 (| digit (any (?A . ?F) (?a . ?f)))) eow)
"Regexp for matching commit hashes.")
(defcustom magithub-hub-executable "hub"
"The hub executable used by Magithub."
:group 'magithub
:package-version '(magithub . "0.1")
:type 'string)
(defvar magithub-debug-mode nil
"When non-nil, echo hub commands before they're executed.")
(defvar magithub-hub-error nil
"When non-nil, this is a message for when hub fails.")
(defmacro magithub-with-hub (&rest body)
`(let ((magit-git-executable magithub-hub-executable)
(magit-pre-call-git-hook nil)
(magit-git-global-arguments nil))
,@body))
(defun magithub--hub-command (magit-function command args)
(unless (executable-find magithub-hub-executable)
(user-error "Hub (hub.github.com) not installed; aborting"))
(unless (file-exists-p "~/.config/hub")
(user-error "Hub hasn't been initialized yet; aborting"))
(when magithub-debug-mode
(message "Calling hub with args: %S %S" command args))
(with-timeout (5 (error "Took too long! %s%S" command args))
(magithub-with-hub (funcall magit-function command args))))
(defun magithub--command (command &optional args)
"Run COMMAND synchronously using `magithub-hub-executable'."
(magithub--hub-command #'magit-run-git command args))
(defun magithub--command-with-editor (command &optional args)
"Run COMMAND asynchronously using `magithub-hub-executable'.
Ensure GIT_EDITOR is set up appropriately."
(magithub--hub-command #'magit-run-git-with-editor command args))
(defun magithub--command-output (command &optional args)
"Run COMMAND synchronously using `magithub-hub-executable'
and returns its output as a list of lines."
(magithub--hub-command #'magit-git-lines command args))
(defun magithub--command-quick (command &optional args)
"Quickly execute COMMAND with ARGS."
(ignore (magithub--command-output command args)))
(defun magithub--meta-new-issue ()
"Open a new Magithub issue.
See /.github/ISSUE_TEMPLATE.md in this repository."
(interactive)
(browse-url "https://github.com/vermiculus/magithub/issues/new"))
(defun magithub--meta-help ()
"Opens Magithub help."
(interactive)
(browse-url "https://gitter.im/vermiculus/magithub"))
(defun magithub-error (err-message tag &optional trace)
"Report a Magithub error."
(setq trace (or trace (with-output-to-string (backtrace))))
(when (y-or-n-p (concat tag " Report? (A bug report will be placed in your clipboard.)"))
(with-current-buffer-window
(get-buffer-create "*magithub issue*")
#'display-buffer-pop-up-window nil
(when (fboundp 'markdown-mode) (markdown-mode))
(insert
(kill-new
(format
"## Automated error report
### Description
%s
### Backtrace
```
%s```
"
(read-string "Briefly describe what you were doing: ")
trace))))
(magithub--meta-new-issue))
(error err-message))
(provide 'magithub-core)
;;; magithub-core.el ends here

View File

@ -0,0 +1,179 @@
;;; magithub-issue.el --- Browse issues with Magithub -*- 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:
;; Jump to issues from `magit-status'!
;;; Code:
(require 'magit)
(require 'magit-section)
(require 'magithub-core)
(require 'magithub-cache)
(magit-define-popup magithub-issues-popup
"Popup console for creating GitHub issues."
'magithub-commands
:man-page "hub"
:options '((?l "Add labels" "--label=" magithub-issue-read-labels))
:actions '((?c "Create new issue" magithub-issue-new)))
(defvar magithub-issue-format
(list :number " %3d "
:title " %s ")
"These properties will be inserted in the order in which their
found. See `magithub-issue--process-line'.")
(defun magithub-issue-new ()
"Create a new issue on GitHub."
(interactive)
(unless (magithub-github-repository-p)
(user-error "Not a GitHub repository"))
(magithub--command-with-editor
"issue" (cons "create" (magithub-issues-arguments))))
(defun magithub-issue-label-list ()
"Return a list of issue labels.
This is a hard-coded list right now."
(list "bug" "duplicate" "enhancement"
"help wanted" "invalid" "question" "wontfix"))
(defun magithub-issue-read-labels (prompt &optional default)
"Read some issue labels and return a comma-separated string.
Available issues are provided by `magithub-issue-label-list'.
DEFAULT is a comma-separated list of issues -- those issues that
are in DEFAULT are not prompted for again."
;; todo: probably need to add DEFAULT to the top here
(s-join
","
(magithub--completing-read-multiple
(format "%s... %s" prompt "Issue labels (or \"\" to quit): ")
(let* ((default-labels (when default (s-split "," default t))))
(cl-set-difference (magithub-issue-label-list) default-labels)))))
(defun magithub-issue--process-line (s)
"Process a line S into an issue.
Returns a plist with the following properties:
:number issue or pull request number
:type either 'pull-request or 'issue
:title the title of the issue or pull request
:url link to issue or pull request"
(let (number title url)
(if (ignore-errors
(with-temp-buffer
(insert s)
(goto-char 0)
(search-forward "]")
(setq number (string-to-number (substring s 0 (point))))
(setq title (substring s (point)
(save-excursion
(goto-char (point-max))
(- (search-backward "(") 2))))
(goto-char (point-max))
(delete-char -2)
(search-backward "(")
(forward-char 2)
(setq url (buffer-substring-no-properties (point) (point-max)))
t))
(list :number number
:type (if (string-match-p (rx "/pull/" (+ digit) eos) url)
'pull-request 'issue)
:title title
:url url)
(magithub-error
"failed to parse issue"
"There was an error parsing issues."))))
(defun magithub-issue-list ()
"Return a list of issues for the current repository."
(magithub-cache :issues
'(with-temp-message "Retrieving issue list..."
(magithub-issue-list--internal))))
(defun magithub-issue-list--internal ()
(sort (mapcar #'magithub-issue--process-line
(magithub--command-output "issue"))
(lambda (a b) (< (plist-get a :number)
(plist-get b :number)))))
(defun magithub-issue--insert (issue)
"Insert an `issue' as a Magit section into the buffer."
(when issue
(magit-insert-section (magithub-issue issue)
(let ((formats (or magithub-issue-format
(list :number " %3d " :title " %s "))))
(while formats
(let ((key (car formats)) (fmt (cadr formats)))
(insert (format fmt (plist-get issue key))))
(setq formats (cddr formats))))
(insert ?\n))))
(defun magithub-issue-browse (issue)
"Visits `issue' in the browser.
Interactively, this finds the issue at point.
If `issue' is nil, open the repository's issues page."
(interactive (list (magit-section-value
(magit-current-section))))
(browse-url
(if (plist-member issue :url)
(plist-get issue :url)
(car (magithub--command-output "browse" '("--url-only" "--" "issues"))))))
(defun magithub-issue-refresh ()
(interactive)
(magithub-cache-clear :issues)
(when (derived-mode-p 'magit-status-mode)
(magit-refresh)))
(defvar magit-magithub-issue-section-map
(let ((map (make-sparse-keymap)))
(define-key map [remap magit-visit-thing] #'magithub-issue-browse)
(define-key map [remap magit-refresh] #'magithub-issue-refresh)
map)
"Keymap for `magithub-issue' sections.")
(defvar magit-magithub-issue-list-section-map
(let ((map (make-sparse-keymap)))
(define-key map [remap magit-visit-thing] #'magithub-issue-browse)
(define-key map [remap magit-refresh] #'magithub-issue-refresh)
map)
"Keymap for `magithub-issue-list' sections.")
(defun magithub-issue--insert-section ()
"Insert GitHub issues if appropriate."
(when (and (magithub-github-repository-p)
(executable-find magithub-hub-executable))
(let* ((issues (magithub-issue-list)))
(magit-insert-section (magithub-issue-list)
(magit-insert-heading "Issues and Pull Requests")
(if issues (mapc #'magithub-issue--insert issues)
(insert " No issues.\n"))))))
;;; Hook into the status buffer
(add-hook 'magit-status-sections-hook #'magithub-issue--insert-section t)
(provide 'magithub-issue)
;;; magithub-issue.el ends here

View File

@ -0,0 +1,12 @@
(define-package "magithub" "20160926.2044" "Magit interfaces for GitHub"
'((emacs "24.3")
(magit "2.8.0")
(git-commit "20160821.1338")
(with-editor "20160828.1025")
(cl-lib "1.0")
(s "20160711.525"))
:url "https://github.com/vermiculus/magithub" :keywords
'("git" "tools" "vc"))
;; Local Variables:
;; no-byte-compile: t
;; End:

View File

@ -0,0 +1,207 @@
;;; magithub.el --- Magit interfaces for GitHub -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
;; Keywords: git, tools, vc
;; Homepage: https://github.com/vermiculus/magithub
;; Package-Requires: ((emacs "24.3") (magit "2.8.0") (git-commit "20160821.1338") (with-editor "20160828.1025") (cl-lib "1.0") (s "20160711.525"))
;; Package-Version: 0.1
;; 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:
;; Magithub is an interface to GitHub using the `hub' utility [1].
;;
;; Integrated into Magit workflows, Magithub allows very easy, very
;; basic GitHub repository management. Supported actions include:
;;
;; - pushing brand-new local repositories up to GitHub
;; - creating forks of existing repositories
;; - submitting pull requests upstream
;; - viewing and creating issues
;;
;; Press `H' in the status buffer to get started -- happy hacking!
;;
;; [1]: https://hub.github.com
;; Requires hub 2.2.8
;;; Code:
(require 'magit)
(require 'magit-process)
(require 'magit-popup)
(require 'git-commit)
(require 'with-editor)
(require 'cl-lib)
(require 's)
(require 'magithub-core)
(require 'magithub-issue)
(require 'magithub-cache)
(require 'magithub-ci)
(magit-define-popup magithub-dispatch-popup
"Popup console for dispatching other Magithub popups."
'magithub-commands
:man-page "hub"
:actions '("Actions"
(?H "Browse on GitHub" magithub-browse)
(?c "Create" magithub-create-popup)
(?f "Fork" magithub-fork-popup)
(?i "Issues" magithub-issues-popup)
(?p "Submit a pull request" magithub-pull-request-popup)
"Meta"
(?g "Refresh all GitHub data" magithub-refresh)
(?& "Request a feature or report a bug" magithub--meta-new-issue)
(?h "Ask for help on Gitter" magithub--meta-help)))
(magit-define-popup-action 'magit-dispatch-popup
?H "Magithub" #'magithub-dispatch-popup ?!)
(define-key magit-status-mode-map
"H" #'magithub-dispatch-popup)
(magit-define-popup magithub-create-popup
"Popup console for creating GitHub repositories."
'magithub-commands
:man-page "hub"
:switches '((?p "Mark as private" "-p"))
:actions '((?c "Create this repository" magithub-create))
:options '((?d "Description" "--description=")
(?h "Homepage" "--homepage=")))
(magit-define-popup magithub-fork-popup
"Popup console for forking GitHub repositories."
'magithub-commands
:man-page "hub"
:switches '((?r "Don't add my fork as a remote in this repository" "--no-remote"))
:actions '((?f "Fork the project at origin" magithub-fork)))
(magit-define-popup magithub-pull-request-popup
"Popup console for creating pull requests on GitHub repositories."
'magithub-commands
:man-page "hub"
:switches '((?f "Ignore unpushed commits" "-f")
(?o "Open in my browser" "-o"))
:options '((?b "Base branch" "--base=" magit-read-branch)
(?h "Head branch" "--head=" magit-read-branch))
:actions '((?P "Submit a pull request" magithub-pull-request))
:default-arguments '("-o"))
(defun magithub-browse ()
"Open the repository in your browser."
(interactive)
(unless (magithub-github-repository-p)
(user-error "Not a GitHub repository"))
(magithub--command-quick "browse"))
(defvar magithub-after-create-messages
'("Don't be shy!"
"Don't let your dreams be dreams!")
"One of these messages will be displayed after you create a
GitHub repository.")
(defun magithub-create ()
"Create the current repository on GitHub."
(interactive)
(message "Creating repository on GitHub...")
(magithub--command "create" (magithub-create-arguments))
(message "Creating repository on GitHub...done! %s"
(nth (random (length magithub-after-create-messages))
magithub-after-create-messages))
(magit-push-popup))
(defun magithub-fork ()
"Fork 'origin' on GitHub."
(interactive)
(unless (magithub-github-repository-p)
(user-error "Not a GitHub repository"))
(when (and (s-equals? "master" (magit-get-current-branch))
(y-or-n-p "Looks like master is checked out. Create a new branch? "))
(call-interactively #'magit-branch-spinoff))
(message "Forking repository on GitHub...")
(magithub--command "fork" (magithub-fork-arguments))
(message "Forking repository on GitHub...done"))
(defun magithub-pull-request ()
"Open a pull request to 'origin' on GitHub."
(interactive)
(unless (magithub-github-repository-p)
(user-error "Not a GitHub repository"))
(let (just-pushed)
(unless (magit-get-push-remote)
(when (y-or-n-p "No push remote defined; push now? ")
(call-interactively #'magit-push-current-to-pushremote)
(setq just-pushed t)))
(unless (magit-get-push-remote)
(user-error "No push remote defined; aborting pull request"))
(unless just-pushed
(when (y-or-n-p "Do you want to push any more commits? ")
(magit-push-popup)))
(magithub--command-with-editor "pull-request" (magithub-pull-request-arguments))))
(defface magithub-issue-warning-face
'((((class color)) :foreground "red"))
"Face used to call out warnings in the issue-create buffer."
:group 'magithub)
(defun magithub-setup-edit-buffer ()
"Perform setup on a hub edit buffer."
(with-editor-mode 1)
(git-commit-setup-font-lock)
(font-lock-add-keywords
nil `((,magithub-hash-regexp (0 'magit-hash t))) t)
(add-hook
(make-local-variable 'with-editor-pre-finish-hook)
(lambda ()
(let ((fill-column (point-max)))
(fill-region (point-min) (point-max))))))
(defun magithub-setup-new-issue-buffer ()
"Setup the buffer created for issue-posting."
(font-lock-add-keywords
nil '(("^# \\(Creating issue for .*\\)" (1 'magithub-issue-warning-face t))) t))
(defvar magithub--file-types
'(("ISSUE_EDITMSG" . issue)
("PULLREQ_EDITMSG" . pull-request))
"File types -- car is the basename of a file in /.git/, cdr is
one of `issue' or `pull-request'.")
(defun magithub--edit-file-type (path)
"Determine the type of buffer this is (if it was created by hub).
Returns `issue', `pull-request', or another non-nil value if
created by hub.
This function will return nil for matches to
`git-commit-filename-regexp'."
(let ((basename (file-name-base path)))
(and path
(s-suffix? "/.git/" (file-name-directory path))
(not (s-matches? git-commit-filename-regexp basename))
(cdr (assoc basename magithub--file-types)))))
(defun magithub-check-buffer ()
"If this is a buffer created by hub, perform setup."
(let ((type (magithub--edit-file-type buffer-file-name)))
(when type
(magithub-setup-edit-buffer)
(when (eq type 'issue)
(magithub-setup-new-issue-buffer)))))
(add-hook 'find-file-hook #'magithub-check-buffer)
(provide 'magithub)
;;; magithub.el ends here