1 ;; gist.el --- Emacs integration for gist.github.com
3 ;; Author: Christian Neukirchen <purl.org/net/chneukirchen>
4 ;; Maintainer: Chris Wanstrath <chris@ozmm.org>
6 ;; Will Farrington <wcfarrington@gmail.com>
11 ;; Created: 21 Jul 2008
12 ;; Keywords: gist git github paste pastie pastebin
14 ;; This file is NOT part of GNU Emacs.
16 ;; This is free software; you can redistribute it and/or modify it under
17 ;; the terms of the GNU General Public License as published by the Free
18 ;; Software Foundation; either version 2, or (at your option) any later
21 ;; This is distributed in the hope that it will be useful, but WITHOUT
22 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
23 ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with GNU Emacs; see the file COPYING. If not, write to the
28 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
29 ;; MA 02111-1307, USA.
33 ;; Uses your local GitHub config if it can find it.
34 ;; See http://github.com/blog/180-local-github-config
38 (eval-when-compile (require 'cl))
41 (defvar github-user nil
42 "If non-nil, will be used as your GitHub username without checking
44 (defvar github-token nil
45 "If non-nil, will be used as your GitHub token without checking
48 (defvar gist-view-gist nil
49 "If non-nil, automatically use `browse-url' to view gists after they're
52 (defvar gist-supported-modes-alist '((action-script-mode . "as")
55 (clojure-mode . "clj")
56 (common-lisp-mode . "lisp")
59 (emacs-lisp-mode . "el")
65 (javascript-mode . "js")
70 (objective-c-mode . "m")
76 (scala-mode . "scala")
79 (smalltalk-mode . "st")
87 (defun* gist-request (url callback &optional params)
88 "Makes a request to `url' asynchronously, notifying `callback' when
89 complete. The github parameters are included in the request. Optionally
90 accepts additional POST `params' as a list of (key . value) conses."
91 (github-with-auth-info login token
92 (let ((url-request-data (gist-make-query-string
94 ("token" . ,token) ,@params)))
95 (url-max-redirecton 5)
96 (url-request-method "POST"))
97 (url-retrieve url callback))))
100 (defun gist-region (begin end &optional private &optional callback)
101 "Post the current region as a new paste at gist.github.com
102 Copies the URL into the kill ring.
104 With a prefix argument, makes a private paste."
106 (let* ((file (or (buffer-file-name) (buffer-name)))
107 (name (file-name-nondirectory file))
108 (ext (or (cdr (assoc major-mode gist-supported-modes-alist))
109 (file-name-extension file)
112 "http://gist.github.com/gists"
113 (or callback 'gist-created-callback)
114 `(,@(if private '(("action_button" . "private")))
115 ("file_ext[gistfile1]" . ,(concat "." ext))
116 ("file_name[gistfile1]" . ,name)
117 ("file_contents[gistfile1]" . ,(buffer-substring begin end))))))
119 (defun gist-created-callback (status)
120 (let ((location (cadr status)))
121 (message "Paste created: %s" location)
123 (browse-url location))
125 (kill-buffer (current-buffer))))
127 (defun gist-make-query-string (params)
128 "Returns a query string constructed from PARAMS, which should be
129 a list with elements of the form (KEY . VALUE). KEY and VALUE
130 should both be strings."
133 (concat (url-hexify-string (car param)) "="
134 (url-hexify-string (cdr param))))
138 (defun gist-region-private (begin end)
139 "Post the current region as a new private paste at gist.github.com
140 Copies the URL into the kill ring."
142 (gist-region begin end t))
144 (defun github-config (key)
145 "Returns a GitHub specific value from the global Git config."
146 (let ((strip (lambda (string)
147 (if (> (length string) 0)
148 (substring string 0 (- (length string) 1)))))
149 (git (executable-find "git")))
150 (funcall strip (shell-command-to-string
151 (concat git " config --global github." key)))))
153 (defun github-set-config (key value)
154 "Sets a GitHub specific value to the global Git config."
155 (let ((git (executable-find "git")))
156 (shell-command-to-string
157 (format git " config --global github.%s %s" key value))))
159 (defun github-auth-info ()
160 "Returns the user's GitHub authorization information.
161 Searches for a GitHub username and token in the global git config,
162 and returns (USERNAME . TOKEN). If nothing is found, prompts
163 for the info then sets it to the git config."
166 ;; If we've been called within a scope that already has this
167 ;; defined, don't take the time to get it again.
168 (if (boundp '*github-auth-info*)
171 (let* ((user (or github-user (github-config "user")))
172 (token (or github-token (github-config "token"))))
175 (setq user (read-string "GitHub username: "))
176 (github-set-config "user" user))
179 (setq token (read-string "GitHub API token: "))
180 (github-set-config "token" token))
184 (defmacro github-with-auth-info (login token &rest body)
185 "Binds the github authentication credentials to `login' and `token'.
186 The credentials are retrieved at most once within the body of this macro."
188 `(let ((*github-auth-info* (github-auth-info)))
189 (destructuring-bind (,login . ,token) *github-auth-info*
193 (defun gist-buffer (&optional private)
194 "Post the current buffer as a new paste at gist.github.com.
195 Copies the URL into the kill ring.
197 With a prefix argument, makes a private paste."
199 (gist-region (point-min) (point-max) private))
202 (defun gist-buffer-private ()
203 "Post the current buffer as a new private paste at gist.github.com.
204 Copies the URL into the kill ring."
206 (gist-region-private (point-min) (point-max)))
209 (defun gist-region-or-buffer (&optional private)
210 "Post either the current region, or if mark is not set, the current buffer as a new paste at gist.github.com
211 Copies the URL into the kill ring.
213 With a prefix argument, makes a private paste."
216 (gist-region (point) (mark) private)
217 (mark-inactive (gist-buffer private))))
220 (defun gist-region-or-buffer-private ()
221 "Post either the current region, or if mark is not set, the current buffer as a new private paste at gist.github.com
222 Copies the URL into the kill ring."
225 (gist-region-private (point) (mark))
226 (mark-inactive (gist-buffer-private))))
228 (defvar gist-fetch-url "http://gist.github.com/%d.txt"
229 "Raw Gist content URL format")
233 "Displays a list of all of the current user's gists in a new buffer."
235 (message "Retrieving list of your gists...")
236 (github-with-auth-info login token
238 (format "http://gist.github.com/api/v1/xml/gists/%s" login)
239 'gist-lists-retrieved-callback)))
241 (defun gist-lists-retrieved-callback (status)
242 "Called when the list of gists has been retrieved. Parses the result
243 and displays the list."
244 (goto-char (point-min))
245 (search-forward "<?xml")
246 (let ((gists (gist-xml-cleanup
247 (xml-parse-region (match-beginning 0) (point-max)))))
248 (kill-buffer (current-buffer))
249 (with-current-buffer (get-buffer-create "*gists*")
250 (goto-char (point-min))
252 (kill-region (point-min) (point-max))
253 (gist-insert-list-header)
254 (mapc 'gist-insert-gist-link (xml-node-children (car gists)))
256 ;; remove the extra newline at the end
257 (delete-backward-char 1))
262 (set-window-buffer nil (current-buffer)))))
264 (defun gist-insert-list-header ()
265 "Creates the header line in the gist list buffer."
267 (insert " ID Created "
268 "Visibility Description \n"))
269 (let ((ov (make-overlay (line-beginning-position) (line-end-position))))
270 (overlay-put ov 'face 'header-line))
273 (defun gist-insert-gist-link (gist)
274 "Inserts a button that will open the given gist when pressed."
275 (let* ((data (gist-parse-gist gist))
276 (repo (string-to-number (car data))))
277 (mapc '(lambda (x) (insert (format " %s " x))) data)
278 (make-text-button (line-beginning-position) (line-end-position)
280 'action 'gist-fetch-button
284 (defun gist-fetch-button (button)
285 "Called when a gist button has been pressed. Fetches and displays the gist."
286 (gist-fetch (button-get button 'repo)))
288 (defun gist-parse-gist (gist)
289 "Returns a list of the gist's attributes for display, given the xml list
291 (let ((repo (gist-child-text 'repo gist))
292 (created-at (gist-child-text 'created-at gist))
293 (description (gist-child-text 'description gist))
294 (public (if (string= (gist-child-text 'public gist) "true")
297 (list repo created-at public description)))
299 (defun gist-child-text (sym node)
300 "Retrieves the text content of a child of a <gist> element."
301 (let* ((children (xml-node-children node)))
302 (car (xml-node-children (assq sym children)))))
304 (defun gist-xml-cleanup (xml-list)
305 "Removes empty strings or whitespace nodes from the `xml-list'.
306 Borrowed from rss.el."
307 (mapcar 'gist-xml-cleanup-node xml-list))
309 (defun gist-xml-cleanup-node (node)
310 "Recursively removes whitespace and empty strings from the given xml `node'.
311 Borrowed from rss.el."
314 (xml-node-attributes node)
316 (dolist (child (xml-node-children node))
318 (or (string-match "\\`[ \t\n]+\\'" child)
320 (push (gist-xml-cleanup-node child) new)))
324 (defun gist-fetch (id)
325 "Fetches a Gist and inserts it into a new buffer
326 If the Gist already exists in a buffer, switches to it"
327 (interactive "nGist ID: ")
329 (let* ((gist-buffer-name (format "*gist %d*" id))
330 (gist-buffer (get-buffer gist-buffer-name)))
331 (if (bufferp gist-buffer)
332 (switch-to-buffer-other-window gist-buffer)
334 (message "Fetching Gist %d..." id)
336 (url-retrieve-synchronously (format gist-fetch-url id)))
337 (with-current-buffer gist-buffer
338 (rename-buffer gist-buffer-name t)
339 (goto-char (point-min))
340 (search-forward-regexp "\n\n")
341 (delete-region (point-min) (point))
342 (set-buffer-modified-p nil))
343 (switch-to-buffer-other-window gist-buffer)))))
346 ;;; gist.el ends here.