Commits

Hugh Giddens committed 66c7936 Merge

Merge branch 'multiple-buffers'

Comments (0)

Files changed (2)

   "Face for the overlay for zebra striped rows."
   :group 'jira-rest-faces)
 
-(defcustom jira-rest-instance (list "" nil nil)
-  "Information on how to connect to JIRA.
-
-Should be a list of three elements: the URL to JIRA (possibly
-including port), your username, and you password - e.g.,
-'(\"https://foo:8000/jira\" \"user\" \"pass\")."
-  :type '(list (string :tag "Host name")
-               (choice (string :tag "User name") (const :tag "No user name" nil))
-               (choice (string :tag "Password") (const :tag "No password" nil)))
+(defvar jira-rest-instance)
+(put 'jira-rest-instance 'variable-documentation "The JIRA instance for the current buffer.")
+(make-variable-buffer-local 'jira-rest-instance)
+
+(defcustom jira-rest-instances nil
+  "Information on known JIRA instances.
+
+Should be a list of instances, where each element is a list of
+four values: a description of the server (used in buffer names),
+the URL of the JIRA instance (possibly including port), your
+username, and your password - e.g. '(\"My instance\"
+\"https://foo:8000/jira\" \"user\" \"pass\")."
+  :type '(repeat
+          (list (string :tag "Description")
+                (string :tag "Host name")
+                (choice (string :tag "User name") (const :tag "No user name" nil))
+                (choice (string :tag "Password") (const :tag "No password" nil))))
   :group 'jira-rest)
 
 (defcustom jira-rest-display-images nil
   "Whether icons (for statuses, users, etc.) should be downloaded
 and displayed."
   :type '(choice (const :tag "Display images." t)
-		 (const :tag "Do not display images." nil))
+                 (const :tag "Do not display images." nil))
   :group 'jira-rest)
 
 (defcustom jira-rest-mode-hook nil
   :group 'jira-rest
   :type 'boolean)
 
-(defvar *jira-rest-request-level* 0
+(defvar jira-rest-request-level 0
   "Used to enforce ordering on actioning async responses.
 
 This stops processing of responses from user actions that are not
 the most recent.")
-
-(defun jira-rest ()
-  "Interact with a JIRA server via the JIRA REST API.
-
-Customisation options are in the `jira-rest' group, in
-particular, see `jira-rest-instance' for controlling which JIRA
-instance is used."
-  (interactive)
-  (when (zerop (length (first jira-rest-instance)))
-    (error "jira-rest-instance should be customised to point to your JIRA instance"))
-  (incf *jira-rest-request-level*)
-
-  (switch-to-buffer "*JIRA*")
-  (jira-rest-mode))
+(make-variable-buffer-local 'jira-rest-request-level)
 
 (define-derived-mode jira-rest-mode special-mode "JIRA"
   "Mode for interacting with JIRA servers.
 
-See also the `jira-rest' function.
+See also `jira-rest-issue-mode'.
 
 \\{jira-rest-mode-map}"
   :group 'jira-rest
 
-  (set (make-local-variable 'revert-buffer-function)
-       'jira-rest-revert-buffer)
-  (jira-rest-history-reset)
-  (jira-rest-list-projects-and-filters)
-  (message "Welcome to jira-rest-mode"))
+  (set (make-local-variable 'revert-buffer-function) 'jira-rest-revert-buffer)
+  (set (make-local-variable 'jira-rest-revert-buffer) nil)
+  (set (make-local-variable 'jira-rest-default-issue) nil))
 
 (suppress-keymap jira-rest-mode-map t)
 (define-key jira-rest-mode-map "l" 'jira-rest-list-projects-and-filters)
 (define-key jira-rest-mode-map "c" 'jira-rest-create-issue)
 (define-key jira-rest-mode-map "o" 'jira-rest-comment-issue)
 (define-key jira-rest-mode-map "a" 'jira-rest-assign-issue)
-(define-key jira-rest-mode-map "n" 'jira-rest-next-comment)
-(define-key jira-rest-mode-map "p" 'jira-rest-previous-comment)
-(define-key jira-rest-mode-map "u" 'jira-rest-update-issue-summary)
-(define-key jira-rest-mode-map "t" 'jira-rest-update-issue-status)
 (define-key jira-rest-mode-map "w" 'jira-rest-watch-issue)
-(define-key jira-rest-mode-map "\C-c\C-b" 'jira-rest-history-back)
-(define-key jira-rest-mode-map "\C-c\C-f" 'jira-rest-history-forward)
 
-(defun jira-rest-session-endpoint ()
-  (concat (first jira-rest-instance) "/rest/auth/latest/session"))
+(define-derived-mode jira-rest-issue-mode jira-rest-mode "JIRA Issue"
+  "Mode derived from `jira-rest-mode' for JIRA issues.
 
-(defun jira-rest-project-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/project"))
+\\{jira-rest-issue-mode-map}"
+  :group 'jira-rest)
 
-(defun jira-rest-project-information-endpoint (key)
-  (concat (first jira-rest-instance) "/rest/api/latest/project/" key))
+(define-key jira-rest-issue-mode-map "n" 'jira-rest-next-comment)
+(define-key jira-rest-issue-mode-map "p" 'jira-rest-previous-comment)
+(define-key jira-rest-issue-mode-map "u" 'jira-rest-update-issue-summary)
+(define-key jira-rest-issue-mode-map "t" 'jira-rest-update-issue-status)
 
-(defun jira-rest-filter-endpoint (filter-id)
-  (format "%s/rest/api/latest/filter/%s" (first jira-rest-instance) filter-id))
+(defun jira-rest-instance-name (instance)
+  (first instance))
+
+(defun jira-rest-instance-url (instance)
+  (second instance))
 
-(defun jira-rest-favourite-filter-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/filter/favourite"))
+(defun jira-rest-instance-username (instance)
+  (third instance))
+
+(defun jira-rest-instance-password (instance)
+  (fourth instance))
+
+(defun jira-rest-endpoint-for-instance (instance endpoint)
+  (concat (jira-rest-instance-url instance)
+          (if (eql (elt instance (1- (length instance))) ?/)
+                       (substring endpoint 1)
+                     endpoint)))
+
+(defun jira-rest-project-endpoint ()
+  "/rest/api/latest/project")
+
+(defun jira-rest-filter-endpoint (filter-id)
+  (format "/rest/api/latest/filter/%s" filter-id))
 
 (defun jira-rest-search-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/search"))
+  "/rest/api/latest/search")
 
 (defun jira-rest-issue-endpoint (id-or-key)
-  (format "%s/rest/api/latest/issue/%s" (first jira-rest-instance) id-or-key))
+  (format "/rest/api/latest/issue/%s" id-or-key))
 
-(defun jira-rest-create-issue-metadata-endpoint (&optional project-id issue-type-id)
-  (let ((prefix (concat (first jira-rest-instance) "/rest/api/latest/issue/createmeta")))
-    (if (and project-id issue-type-id)
-        (format "%s?projectIds=%s&issuetypeIds=%s&expand=projects.issuetypes.fields"
-                prefix project-id issue-type-id)
-      prefix)))
+(defun jira-rest-create-issue-metadata-endpoint ()
+  "/rest/api/latest/issue/createmeta")
 
 (defun jira-rest-create-issue-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/issue"))
+  "/rest/api/latest/issue")
 
 (defun jira-rest-comment-endpoint (id-or-key)
-  (format "%s/rest/api/latest/issue/%s/comment" (first jira-rest-instance) id-or-key))
+  (format "/rest/api/latest/issue/%s/comment" id-or-key))
 
 (defun jira-rest-assignable-user-endpoint (issue-key user-search)
-  (format "%s/rest/api/latest/user/assignable/search?issueKey=%s&username=%s"
-          (first jira-rest-instance) issue-key user-search))
+  (format "/rest/api/latest/user/assignable/search?issueKey=%s&username=%s" issue-key user-search))
 
 (defun jira-rest-issue-transitions-endpoint (id-or-key)
-  (format "%s/rest/api/latest/issue/%s/transitions" (first jira-rest-instance) id-or-key))
+  (format "/rest/api/latest/issue/%s/transitions" id-or-key))
 
 (defun jira-rest-server-info-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/serverInfo"))
+  "/rest/api/latest/serverInfo")
 
 (defun jira-rest-resolutions-endpoint ()
-  (concat (first jira-rest-instance) "/rest/api/latest/resolution"))
+  "/rest/api/latest/resolution")
 
 (defun jira-rest-watchers-endpoint (id-or-key &optional user-to-delete)
-  (format "%s/rest/api/latest/issue/%s/watchers%s"
-          (first jira-rest-instance)
+  (format "/rest/api/latest/issue/%s/watchers%s"
           id-or-key
           (if user-to-delete (concat "?username=" user-to-delete) "")))
 
-(defun jira-rest-is-logged-in-p ()
-  "Determines whether a response was served to a authenticated user. "
-  (save-excursion
-    (save-restriction
-      (mail-narrow-to-head)
-      (string= (mail-fetch-field "X-Seraph-LoginReason" t) "OK"))))
-
-(defun jira-rest-should-retry-after-login-p ()
-  (and (second jira-rest-instance)
-       (third jira-rest-instance)
-       (not (jira-rest-is-logged-in-p))))
+(defun jira-rest-auth-token (instance)
+  (concat "Basic "
+          (base64-encode-string (concat (jira-rest-instance-username instance)
+                                        ":"
+                                        (jira-rest-instance-password instance)))))
 
 (defmacro with-jira-rest-response (&rest body)
   (let ((buffer (gensym)))
 (defmacro jira-rest-retrieve (url callback args)
   `(url-retrieve ,url ,callback ,args ,@(if (eql emacs-major-version 23) nil (list t))))
 
-(defun jira-rest-request (url-parameters callback args)
+(defun jira-rest-request (instance url-parameters callback args)
   "Submits a request to JIRA.
 
 URL-PARAMETERS should be a list of four elements, containing as
 response body.
 
 Wraps `url-retrieve', which should be seen for more details."
-  ;; We do this asinine request, re-auth if failed, retry stuff because the
-  ;; authenticated sessions end with no warning from our end. We're using
-  ;; session-based authentication instead of HTTP basic authentication (as
-  ;; the documentation says we should be) because doing it this way is
-  ;; dramatically faster (~ 6x against OnDemand).
-  (labels ((got-first-attempt (status url-parameters callback args)
-             (with-jira-rest-response
-              (destructuring-bind (&optional error-symbol . data) (plist-get status :error)
-                (when (and error-symbol (not (= (second data) 401)))
-                  (signal error-symbol data))
-                (if (or error-symbol (jira-rest-should-retry-after-login-p))
-                    (let ((url-request-method "POST")
-                          (url-request-extra-headers '(("Content-Type" . "application/json")))
-                          (url-request-data (json-encode `((username . ,(second jira-rest-instance))
-                                                           (password . ,(third jira-rest-instance))))))
-                      (jira-rest-retrieve (jira-rest-session-endpoint)
-					  'got-login-response
-					  (list url-parameters callback args)))
-                  (apply callback (jira-rest-parse-response status) args)))))
-           (got-login-response (status url-parameters callback args)
-             (with-jira-rest-response
-              (if (and (null (plist-get status :error))
-                       (jira-rest-is-logged-in-p))
-                  (destructuring-bind (url url-request-method url-request-extra-headers url-request-data)
-                      url-parameters
-                    (jira-rest-retrieve url 'got-second-attempt (list callback args)))
-                (error "Unable to log in: %S" status))))
-           (got-second-attempt (status callback args)
-             (with-jira-rest-response
-              (destructuring-bind (&optional error-symbol . data) (plist-get status :error)
-                (when error-symbol
-                  (signal error-symbol data)))
-              (apply callback (jira-rest-parse-response status) args))))
-    (destructuring-bind (url url-request-method url-request-extra-headers url-request-data)
-        url-parameters
-      (jira-rest-retrieve url 'got-first-attempt (list url-parameters callback args)))))
-
-(defun jira-rest-get (url callback &optional args)
-  (jira-rest-request (list url nil nil nil) callback args))
-
-(defun jira-rest-post (url data callback &optional args)
-  (jira-rest-request (list url "POST" '(("Content-Type" . "application/json")) (json-encode data))
+  (destructuring-bind (url url-request-method url-request-extra-headers url-request-data)
+      url-parameters
+    (push (cons "Authorization" (jira-rest-auth-token instance)) url-request-extra-headers)
+    (jira-rest-retrieve (jira-rest-endpoint-for-instance instance url)
+                        (lambda (status callback args)
+                          (with-jira-rest-response
+                           (destructuring-bind (&optional error-symbol . data) (plist-get status :error)
+                             (when error-symbol
+                               (signal error-symbol data)))
+                           (apply callback (jira-rest-parse-response status) args)))
+                        (list callback args))))
+
+(defun jira-rest-get (instance url callback &optional args)
+  (jira-rest-request instance (list url nil nil nil) callback args))
+
+(defun jira-rest-post (instance url data callback &optional args)
+  (jira-rest-request instance (list url "POST" '(("Content-Type" . "application/json")) (json-encode data))
                      callback args))
 
-(defun jira-rest-put (url data callback &optional args)
-  (jira-rest-request (list url "PUT" '(("Content-Type" . "application/json")) (json-encode data))
+(defun jira-rest-put (instance url data callback &optional args)
+  (jira-rest-request instance (list url "PUT" '(("Content-Type" . "application/json")) (json-encode data))
                      callback args))
 
-(defun jira-rest-delete (url data callback &optional args)
-  (jira-rest-request (list url "DELETE" '(("Content-Type" . "application/json")) (json-encode data))
+(defun jira-rest-delete (instance url data callback &optional args)
+  (jira-rest-request instance (list url "DELETE" '(("Content-Type" . "application/json")) (json-encode data))
                      callback args))
 
-(defun jira-rest-list-projects-and-filters ()
-  "Lists the projects and favourited filters of the JIRA instance."
-  (interactive)
-  (incf *jira-rest-request-level*)
-  (labels ((got-server-info (&rest args) ;; (info target-buffer)
-             (jira-rest-get (jira-rest-project-endpoint) 'got-projects args))
-           (got-projects (&rest args) ;; (projects info target-buffer)
-             (jira-rest-get (jira-rest-favourite-filter-endpoint) 'got-filters args))
+(defun jira-rest-required-read (prompt &optional completions)
+  (let ((initial (concat prompt ": "))
+        (subsequent (concat prompt ": (required): ")))
+    (do ((str (if completions
+                  (completing-read initial completions nil t)
+                (read-string initial))
+              (if completions
+                  (completing-read subsequent completions nil t)
+                (read-string subsequent))))
+        ((not (equal str "")) str))))
+
+(defun jira-rest-infer-issue ()
+  (or (bound-and-true-p jira-rest-default-issue)
+      (jira-rest-required-read "Issue key")))
+
+(defun jira-rest-infer-instance ()
+  (let ((instance-count (length jira-rest-instances)))
+    (cond
+     ((bound-and-true-p jira-rest-instance) jira-rest-instance)
+     ((zerop instance-count) (error "Please customize `jira-rest-instances'"))
+     ((eql instance-count 1) (first jira-rest-instances))
+     (t (assoc (jira-rest-required-read "Instance" (mapcar 'first jira-rest-instances))
+               jira-rest-instances)))))
+
+(defun jira-rest-list-projects-and-filters (instance)
+  "Lists the projects and favourited filters of INSTANCE."
+  (interactive (list (jira-rest-infer-instance)))
+  (switch-to-buffer (format "*%s*" (jira-rest-instance-name instance)))
+  (unless (eq major-mode 'jira-rest-mode)
+    (jira-rest-mode))
+  (incf jira-rest-request-level)
+  (setf jira-rest-revert-buffer (list 'jira-rest-list-projects-and-filters instance)
+        jira-rest-instance instance)
+  (labels ((got-server-info (info target-buffer)
+             (with-current-buffer target-buffer
+               (jira-rest-get jira-rest-instance
+                              (jira-rest-project-endpoint)
+                              'got-projects
+                              (list info target-buffer))))
+           (got-projects (projects info target-buffer)
+             (with-current-buffer target-buffer
+               (jira-rest-get jira-rest-instance
+                              (jira-rest-filter-endpoint "favourite")
+                              'got-filters
+                              (list projects info target-buffer))))
            (got-filters (filters projects info target-buffer)
              (let ((row-count 0)
                    avatar-requests)
                     (insert-text-button (cdr (assoc 'key project))
                                         'action (lambda (button)
                                                   (jira-rest-search-project-issues
-                                                   (button-get button 'jira-rest-project-key) ""))
+                                                   jira-rest-instance
+                                                   (button-get button 'jira-rest-project-key)
+                                                   ""))
                                         'follow-link 'mouse-face
                                         'jira-rest-project-key (cdr (assoc 'key project)))
                     (beginning-of-line)
                     (beginning-of-line)
                     (insert-text-button (cdr (assoc 'id filter))
                                         'action (lambda (button)
-                                                  (jira-rest-list-issues (button-get button 'jira-rest-filter-key)))
+                                                  (jira-rest-list-issues
+                                                   jira-rest-instance
+                                                   (button-get button 'jira-rest-filter-key)))
                                         'follow-link 'mouse-face
                                         'jira-rest-filter-key (cdr (assoc 'id filter)))
                     (beginning-of-line)
                     (when (and jira-rest-zebra-stripe-rows (oddp row-count))
                       (overlay-put (make-overlay row-start (point)) 'face 'jira-rest-zebra-stripe-face)))
                   (incf row-count))
-                (jira-rest-history-record 'jira-rest-list-projects-and-filters)
                 (jira-rest-load-images-async (nreverse avatar-requests))))))
-    (jira-rest-get (jira-rest-server-info-endpoint) 'got-server-info (list (current-buffer)))))
+    (jira-rest-get instance
+                   (jira-rest-server-info-endpoint)
+                   'got-server-info
+                   (list (current-buffer)))))
 
 (defun jira-rest-load-images-async (requests)
   (labels ((after-timeout (target-buffer requests request-level)
-             (dolist (request requests)
-               (destructuring-bind (url . insertion-points) request
-                 (jira-rest-retrieve url 'got-avatar (list target-buffer insertion-points request-level)))))
+             (with-current-buffer target-buffer
+               (dolist (request requests)
+                 (destructuring-bind (url . insertion-points) request
+                   (let ((url-request-extra-headers `(("Authorization" . ,(jira-rest-auth-token jira-rest-instance)))))
+                     (jira-rest-retrieve url 'got-avatar (list target-buffer insertion-points request-level)))))))
            (got-avatar (status target-buffer insertion-points request-level)
              (let ((buffer (current-buffer)))
                (unwind-protect
                    (when (and (null (plist-get status :error))
-                              (eql request-level *jira-rest-request-level*)
                               (buffer-live-p target-buffer))
                      (jira-rest-skip-header)
                      (let ((image (condition-case _ (create-image (buffer-substring (point) (point-max)) nil t)
                          (destructuring-bind (_ . props) image
                            (save-excursion
                              (set-buffer target-buffer)
-                             (let ((buffer-read-only nil)
-                                   (inhibit-point-motion-hooks t))
-                               (dolist (insertion-point insertion-points)
-                                 (goto-char insertion-point)
-                                 (remove-text-properties insertion-point (+ insertion-point 2)
-                                                         '(invisible nil))
-                                 (put-text-property insertion-point (1+ insertion-point)
-                                                    'display `(image :ascent center ,@props))
-                                 (end-of-line)
-                                 ;; XXX This should be smarter. Not decrease the line height if it's
-                                 ;; already set larger, and to not have a hardcoded height
-                                 (add-text-properties (point) (1+ (point)) '(line-height 17)))))))))
+                             (when (eql request-level jira-rest-request-level)
+                               (let ((buffer-read-only nil)
+                                     (inhibit-point-motion-hooks t))
+                                 (dolist (insertion-point insertion-points)
+                                   (goto-char insertion-point)
+                                   (remove-text-properties insertion-point (+ insertion-point 2)
+                                                           '(invisible nil))
+                                   (put-text-property insertion-point (1+ insertion-point)
+                                                      'display `(image :ascent center ,@props))
+                                   (end-of-line)
+                                   ;; XXX This should be smarter. Not decrease the line height if it's
+                                   ;; already set larger, and to not have a hardcoded height
+                                   (add-text-properties (point) (1+ (point)) '(line-height 17))))))))))
                  (kill-buffer buffer)))))
     (let (optimised)
       (dolist (request requests)
             (if record
                 (push position (cdr record))
               (push request optimised)))))
-      (run-at-time 0 nil 'after-timeout (current-buffer) optimised *jira-rest-request-level*))))
+      (run-at-time 0 nil 'after-timeout (current-buffer) optimised jira-rest-request-level))))
 
 (defmacro with-jira-rest-buffer (target-buffer &rest body)
   `(with-current-buffer target-buffer
-     (let ((buffer-read-only nil))
+     (let ((buffer-read-only nil)
+           (original (point)))
        (delete-region (point-min) (point-max))
        ,@body
        (goto-char (point-max))
        (delete-horizontal-space)
-       (destructuring-bind (has-past has-future) (jira-rest-has-history)
-         (when (or has-past has-future)
-           (goto-char (point-max))
-           (insert "\n\n")
-           (when has-past
-             (insert-text-button "[Back]"
-                                 'action (lambda (button)
-                                           (jira-rest-history-back))
-                                 'follow-link 'mouse-face))
-           (when (and has-past has-future)
-             (insert " "))
-           (when has-future
-             (insert-text-button "[Forward]"
-                                 'action (lambda (button)
-                                           (jira-rest-history-forward))
-                                 'follow-link 'mouse-face))
-           (insert "\n")))
-        (goto-char (point-min)))))
+       (goto-char original))))
 
 (defun jira-rest-jql-text-search (query)
   (if (zerop (length query))
               quote-escaped quote-escaped quote-escaped))))
 
 ;;; XXX We seem to be getting back much richer documents here than the documentation says we should be
-(defun jira-rest-search-issues (text)
-  "Displays a list of issues matching TEXT.
+(defun jira-rest-search-issues (instance text)
+  "Displays a list of issues matching TEXT on INSTANCE.
 
 Summary, description, and comment fields are searched."
-  (interactive "sSearch: ")
-  (incf *jira-rest-request-level*)
-  (jira-rest-post (jira-rest-search-endpoint)
-                  `((jql . ,(or (jira-rest-jql-text-search text) ""))
-                    (maxResults . 20))
-                  (lambda (search-results text max-results target-buffer)
-                    (with-jira-rest-buffer target-buffer
-                     (insert (propertize "Issue search\n" 'face 'jira-rest-title-face) "\n")
-                     (unless (zerop (length text))
-                       (insert (propertize "Query text\n" 'face 'jira-rest-heading-face) text "\n\n"))
-                     (jira-rest-display-issues (cdr (assoc 'issues search-results)))
-                     (jira-rest-history-record 'jira-rest-search-issues text)))
-                  (list text 20 (current-buffer))))
-
-(defun jira-rest-search-project-issues (project text)
-  "Displays a list of issues matching TEXT in PROJECT.
+  (interactive (list (jira-rest-infer-instance)
+                     (read-string "Search: ")))
+  (switch-to-buffer (format "*%s Search*" (jira-rest-instance-name instance)))
+  (unless (eq major-mode 'jira-rest-mode)
+    (jira-rest-mode))
+  (incf jira-rest-request-level)
+  (setf jira-rest-instance instance
+        jira-rest-revert-buffer
+        (list 'jira-rest-post
+              instance
+              (jira-rest-search-endpoint)
+              `((jql . ,(or (jira-rest-jql-text-search text) ""))
+                (maxResults . 20))
+              (lambda (search-results text max-results target-buffer)
+                (with-jira-rest-buffer target-buffer
+                  (insert (propertize "Issue search\n" 'face 'jira-rest-title-face) "\n")
+                  (unless (zerop (length text))
+                    (insert (propertize "Query text\n" 'face 'jira-rest-heading-face) text "\n\n"))
+                  (jira-rest-display-issues (cdr (assoc 'issues search-results)))))
+              (list text 20 (current-buffer))))
+  (jira-rest-revert-buffer))
+
+(defun jira-rest-search-project-issues (instance project text)
+  "Displays a list of issues matching TEXT in PROJECT on INSTANCE.
 
 Summary, description, and comment fields are searched."
-  (interactive "sProject: \nsQuery: ")
+  (interactive (list (jira-rest-infer-instance)
+                     (read-string "Project: ")
+                     (read-string "Query: ")))
   ;; TODO should only request the fields needed?
   ;; TODO should do a completing-read for projects.
-  (incf *jira-rest-request-level*)
-  (jira-rest-post (jira-rest-search-endpoint)
-                  `((jql . ,(let ((text-query (jira-rest-jql-text-search text)))
-                              (if (null text-query)
-                                  (format "project = \"%s\"" project)
-                                (format "project = \"%s\" AND (%s)" project text-query))))
-                    (maxResults . 20))
-                  (lambda (search-results project text max-results target-buffer)
-                    (with-jira-rest-buffer target-buffer
-                     (insert (propertize (concat (upcase project) " project issue search\n")
-                                         'face 'jira-rest-title-face) "\n")
-                     (unless (zerop (length text))
-                       (insert (propertize "Query text\n" 'face 'jira-rest-heading-face) text "\n\n"))
-                     (jira-rest-display-issues (cdr (assoc 'issues search-results)))
-                     (jira-rest-history-record 'jira-rest-search-project-issues project text)))
-                  (list project text 20 (current-buffer))))
+  (switch-to-buffer (format "*%s Search*" (jira-rest-instance-name instance)))
+  (unless (eq major-mode 'jira-rest-mode)
+    (jira-rest-mode))
+  (incf jira-rest-request-level)
+  (setf jira-rest-instance instance
+        jira-rest-revert-buffer
+        (list 'jira-rest-post
+              instance
+              (jira-rest-search-endpoint)
+              `((jql . ,(let ((text-query (jira-rest-jql-text-search text)))
+                          (if (null text-query)
+                              (format "project = \"%s\"" project)
+                            (format "project = \"%s\" AND (%s)" project text-query))))
+                (maxResults . 20))
+              (lambda (search-results project text max-results target-buffer)
+                (with-jira-rest-buffer target-buffer
+                  (insert (propertize (concat (upcase project) " project issue search\n")
+                                      'face 'jira-rest-title-face) "\n")
+                  (unless (zerop (length text))
+                    (insert (propertize "Query text\n" 'face 'jira-rest-heading-face) text "\n\n"))
+                  (jira-rest-display-issues (cdr (assoc 'issues search-results)))))
+              (list project text 20 (current-buffer))))
+  (jira-rest-revert-buffer))
 
 ;; TODO: look at tabulated-list-mode for this stuff in info node
 ;; (elisp)Basic Major Modes
                             (insert (cdr (assoc 'key issue)))
                             (point))
                           'action (lambda (button)
-                                    (jira-rest-show-issue (button-get button 'jira-rest-issue-key)))
+                                    (jira-rest-show-issue
+                                     jira-rest-instance
+                                     (button-get button 'jira-rest-issue-key)))
                           'follow-link 'mouse-face
                           'jira-rest-issue-key (cdr (assoc 'key issue)))
         (beginning-of-line)
       (incf row-count))
     (jira-rest-load-images-async (nreverse icon-requests))))
 
-(defun jira-rest-list-issues (filter-id)
-  "Displays a list of issues matching the filter specified by FILTER-ID."
-  (interactive "nFilter ID: ")
+(defun jira-rest-list-issues (instance filter-id)
+  "Displays a list of issues matching the filter specified by FILTER-ID on INSTANCE."
+  (interactive (list (jira-rest-infer-instance)
+                     (read-number "Filter ID: ")))
   ;; TODO should do a completing read for the filter id
-  (incf *jira-rest-request-level*)
+  (switch-to-buffer (format "*%s Search*" (jira-rest-instance-name instance)))
+  (unless (eq major-mode 'jira-rest-mode)
+    (jira-rest-mode))
+  (incf jira-rest-request-level)
   (labels ((got-filter (filter filter-id target-buffer)
-             (jira-rest-post (jira-rest-search-endpoint)
-                             `((jql . ,(format "filter = %s" filter-id)))
-                             'got-search-results
-                             (list filter filter-id target-buffer)))
+             (with-current-buffer target-buffer
+               (jira-rest-post jira-rest-instance
+                               (jira-rest-search-endpoint)
+                               `((jql . ,(format "filter = %s" filter-id)))
+                               'got-search-results
+                               (list filter filter-id target-buffer))))
            (got-search-results (search-results filter filter-id target-buffer)
              (let ((issues (cdr (assoc 'issues search-results))))
                (with-jira-rest-buffer target-buffer
                 (when (cdr (assoc 'description filter))
                   (insert (propertize "Description\n" 'face 'jira-rest-heading-face)
                           (cdr (assoc 'description filter)) "\n\n"))
-                (jira-rest-display-issues issues)
-                (jira-rest-history-record 'jira-rest-list-issues filter-id)))))
-    (jira-rest-get (jira-rest-filter-endpoint filter-id)
-                   'got-filter
-                   (list filter-id (current-buffer)))))
+                (jira-rest-display-issues issues)))))
+    (setf jira-rest-instance instance
+          jira-rest-revert-buffer
+          (list 'jira-rest-get
+                instance
+                (jira-rest-filter-endpoint filter-id)
+                'got-filter
+                (list filter-id (current-buffer))))
+    (jira-rest-revert-buffer)))
 
 (defun jira-rest-format-date (date)
   (let ((r (rx string-start
                             (if (equal (match-string 7 date) "+") 60 -60))))))
       date)))
 
-(defun jira-rest-show-issue (issue-key)
-  "Displays the issue with the specified ISSUE-KEY."
-  (interactive "sIssue Key: ")
+(defun jira-rest-show-issue (instance issue-key)
+  "Displays the issue with the specified ISSUE-KEY on INSTANCE."
+  (interactive (list (jira-rest-infer-instance)
+                     (jira-rest-required-read "Issue Key")))
+  (setf issue-key (upcase issue-key))
+  (switch-to-buffer (format "*%s %s*" (jira-rest-instance-name instance) issue-key))
+  (unless (eq major-mode 'jira-rest-issue-mode)
+    (jira-rest-issue-mode))
+  (setf jira-rest-default-issue issue-key)
+
   ;; TODO It might be cool to do a completing read here, maybe on just the project.
   ;; TODO How does this handle non-existant keys?
-  (incf *jira-rest-request-level*)
-  (jira-rest-get (jira-rest-issue-endpoint issue-key)
-                 (lambda (issue issue-key target-buffer)
-                   (let* ((fields (cdr (assoc 'fields issue)))
-                          (project (cdr (assoc 'project fields)))
-                          (issue-type (cdr (assoc 'issuetype fields)))
-                          (status (cdr (assoc 'status fields)))
-                          (resolution (cdr (assoc 'resolution fields)))
-                          (priority (cdr (assoc 'priority fields)))
-                          (assignee (cdr (assoc 'assignee fields)))
-                          (reporter (cdr (assoc 'reporter fields)))
-                          (watches (cdr (assoc 'watches fields)))
-                          (comments (cdr (assoc 'comments (cdr (assoc 'comment fields)))))
-                          (components (loop for component in (cdr (assoc 'components fields))
-                                            collect (cdr (assoc 'name component))))
-                          (label (cdr (assoc 'labels fields)))
-                          (affect-versions (loop for affected in (cdr (assoc 'versions fields))
-                                                 collect (cdr (assoc 'name affected))))
-                          (fix-versions (loop for fix-version in (cdr (assoc 'fixVersions fields))
-                                              collect (cdr (assoc 'name fix-version))))
-                          icon-requests)
-                     (with-jira-rest-buffer target-buffer
-                      (insert (propertize (format "%s: %s\n"
-                                                  (cdr (assoc 'key issue))
-                                                  (cdr (assoc 'summary fields)))
-                                          'face 'jira-rest-title-face)
-                              "\n")
-                      (let* ((fields (list (list "Type"
-                                                 (cdr (assoc 'name issue-type))
-                                                 (cdr (assoc 'iconUrl issue-type)))
-                                           (list "Status" (cdr (assoc 'name status)) (cdr (assoc 'iconUrl status)))
-                                           (list "Resolution"
-                                                 (cdr (assoc 'name resolution)) ;; XXX icon not in json?
-                                                 )
-                                           (list "Priority"
-                                                 (cdr (assoc 'name priority))
-                                                 (cdr (assoc 'iconUrl priority)))
-                                           (list "Assignee"
-                                                 (cdr (assoc 'displayName assignee))
-                                                 (cdr (assoc '16x16 (cdr (assoc 'avatarUrls assignee)))))
-                                           (list "Reporter"
-                                                 (cdr (assoc 'displayName reporter))
-                                                 (cdr (assoc '16x16 (cdr (assoc 'avatarUrls reporter)))))
-                                           (list "Created"
-                                                 (jira-rest-format-date (cdr (assoc 'created fields))))
-                                           (list "Updated"
-                                                 (jira-rest-format-date (cdr (assoc 'updated fields))))
-                                           (list "Watchers"
-                                                 (number-to-string (cdr (assoc 'watchCount watches))))
-                                           (list "Components" (mapconcat 'identity components ", "))
-                                           (list "Labels" (mapconcat 'identity label ", "))
-                                           (list "Affects Versions"
-                                                 (mapconcat 'identity affect-versions ", "))
-                                           (list "Fix Versions"
-                                                 (mapconcat 'identity fix-versions ", "))))
-                             (populated-fields (delete-if (lambda (c) (zerop (length c))) fields
-                                                          :key 'second))
-                             (max-header-length (loop for field in populated-fields
-                                                      maximize (length (car field)))))
-                        (dolist (field populated-fields)
-                          (destructuring-bind (header content &optional icon) field
-                            (insert (propertize header 'face 'jira-rest-issue-info-header-face) ": ")
-                            (insert-char ?  (- max-header-length (length header)))
-                            (when (and jira-rest-display-images icon)
-                              (push (list icon (point)) icon-requests))
-                            (insert (propertize "  " 'invisible t)
-                                    content "\n"))))
-                      (insert "\n" (or (jira-rest-strip-cr (cdr (assoc 'description fields))) "") "\n\n")
-
-                      (let ((count 1))
-                        (dolist (comment comments)
-                          (let* ((author (cdr (assoc 'author comment)))
-                                 (icon (cdr (assoc '16x16 (cdr (assoc 'avatarUrls author))))))
-                            (insert (propertize
-                                     (concat "Comment #" (int-to-string count)
-                                             " - ")
-                                     'face 'jira-rest-comment-header-face))
-                            (when (and jira-rest-display-images icon)
-                              (push (list icon (point)) icon-requests))
-                            (insert (propertize "  " 'invisible t)
-                                    (propertize
-                                     (concat (cdr (assoc 'displayName author))
-                                             " - "
-                                             (jira-rest-format-date (cdr (assoc 'created comment)))
-                                             "\n")
-                                     'face 'jira-rest-comment-header-face)
-                                    (jira-rest-strip-cr (cdr (assoc 'body comment)))
-                                    "\n\n"))
-                          (incf count)))
-                      (jira-rest-history-record 'jira-rest-show-issue issue-key)
-                      (jira-rest-load-images-async (nreverse icon-requests)))))
-                 (list issue-key (current-buffer))))
+  (incf jira-rest-request-level)
+  (setf jira-rest-instance instance
+        jira-rest-revert-buffer
+        (list 'jira-rest-get
+              instance
+              (jira-rest-issue-endpoint issue-key)
+              (lambda (issue issue-key target-buffer)
+                (let* ((fields (cdr (assoc 'fields issue)))
+                       (project (cdr (assoc 'project fields)))
+                       (issue-type (cdr (assoc 'issuetype fields)))
+                       (status (cdr (assoc 'status fields)))
+                       (resolution (cdr (assoc 'resolution fields)))
+                       (priority (cdr (assoc 'priority fields)))
+                       (assignee (cdr (assoc 'assignee fields)))
+                       (reporter (cdr (assoc 'reporter fields)))
+                       (watches (cdr (assoc 'watches fields)))
+                       (comments (cdr (assoc 'comments (cdr (assoc 'comment fields)))))
+                       (components (loop for component in (cdr (assoc 'components fields))
+                                         collect (cdr (assoc 'name component))))
+                       (label (cdr (assoc 'labels fields)))
+                       (affect-versions (loop for affected in (cdr (assoc 'versions fields))
+                                              collect (cdr (assoc 'name affected))))
+                       (fix-versions (loop for fix-version in (cdr (assoc 'fixVersions fields))
+                                           collect (cdr (assoc 'name fix-version))))
+                       icon-requests)
+                  (with-jira-rest-buffer target-buffer
+                    (insert (propertize (format "%s: %s\n"
+                                                (cdr (assoc 'key issue))
+                                                (cdr (assoc 'summary fields)))
+                                        'face 'jira-rest-title-face)
+                            "\n")
+                    (let* ((fields (list (list "Type"
+                                               (cdr (assoc 'name issue-type))
+                                               (cdr (assoc 'iconUrl issue-type)))
+                                         (list "Status" (cdr (assoc 'name status)) (cdr (assoc 'iconUrl status)))
+                                         (list "Resolution"
+                                               (cdr (assoc 'name resolution)) ;; XXX icon not in json?
+                                               )
+                                         (list "Priority"
+                                               (cdr (assoc 'name priority))
+                                               (cdr (assoc 'iconUrl priority)))
+                                         (list "Assignee"
+                                               (cdr (assoc 'displayName assignee))
+                                               (cdr (assoc '16x16 (cdr (assoc 'avatarUrls assignee)))))
+                                         (list "Reporter"
+                                               (cdr (assoc 'displayName reporter))
+                                               (cdr (assoc '16x16 (cdr (assoc 'avatarUrls reporter)))))
+                                         (list "Created"
+                                               (jira-rest-format-date (cdr (assoc 'created fields))))
+                                         (list "Updated"
+                                               (jira-rest-format-date (cdr (assoc 'updated fields))))
+                                         (list "Watchers"
+                                               (number-to-string (cdr (assoc 'watchCount watches))))
+                                         (list "Components" (mapconcat 'identity components ", "))
+                                         (list "Labels" (mapconcat 'identity label ", "))
+                                         (list "Affects Versions"
+                                               (mapconcat 'identity affect-versions ", "))
+                                         (list "Fix Versions"
+                                               (mapconcat 'identity fix-versions ", "))))
+                           (populated-fields (delete-if (lambda (c) (zerop (length c))) fields
+                                                        :key 'second))
+                           (max-header-length (loop for field in populated-fields
+                                                    maximize (length (car field)))))
+                      (dolist (field populated-fields)
+                        (destructuring-bind (header content &optional icon) field
+                          (insert (propertize header 'face 'jira-rest-issue-info-header-face) ": ")
+                          (insert-char ?  (- max-header-length (length header)))
+                          (when (and jira-rest-display-images icon)
+                            (push (list icon (point)) icon-requests))
+                          (insert (propertize "  " 'invisible t)
+                                  content "\n"))))
+                    (insert "\n" (or (jira-rest-strip-cr (cdr (assoc 'description fields))) "") "\n\n")
+                    (let ((count 1))
+                      (dolist (comment comments)
+                        (let* ((author (cdr (assoc 'author comment)))
+                               (icon (cdr (assoc '16x16 (cdr (assoc 'avatarUrls author))))))
+                          (insert (propertize
+                                   (concat "Comment #" (int-to-string count)
+                                           " - ")
+                                   'face 'jira-rest-comment-header-face))
+                          (when (and jira-rest-display-images icon)
+                            (push (list icon (point)) icon-requests))
+                          (insert (propertize "  " 'invisible t)
+                                  (propertize
+                                   (concat (cdr (assoc 'displayName author))
+                                           " - "
+                                           (jira-rest-format-date (cdr (assoc 'created comment)))
+                                           "\n")
+                                   'face 'jira-rest-comment-header-face)
+                                  (jira-rest-strip-cr (cdr (assoc 'body comment)))
+                                  "\n\n"))
+                        (incf count)))
+                    (jira-rest-load-images-async (nreverse icon-requests)))))
+              (list issue-key (current-buffer))))
+  (jira-rest-revert-buffer))
 
 (defun jira-rest-strip-cr (string)
   (when string (replace-regexp-in-string "\r" "" string)))
         (beginning-of-line))
     (goto-char 0)))
 
-(defun jira-rest-create-issue ()
-  "Creates a new issue."
-  (interactive)
+(defun jira-rest-create-issue (instance)
+  "Creates a new issue on INSTANCE."
+  (interactive (list (jira-rest-infer-instance)))
   ;; This should take project, issue type, summary, and description as arguments
   (labels ((project-key (project) (cdr (assoc 'key project)))
            (issue-type-name (issue-type) (cdr (assoc 'name issue-type)))
-           (completing-required-read (prompt options)
-             (do ((s (completing-read (concat prompt ": ") options nil t)
-                     (completing-read (concat prompt " (required): ") options nil t)))
-                 ((not (equal s "")) s)))
-           (got-projects-and-issues (issue-creation-metadata)
+           (got-projects-and-issues (issue-creation-metadata instance)
              (let* ((projects (cdr (assoc 'projects issue-creation-metadata)))
                     (project-key-list (mapcar 'project-key projects))
-                    (key (completing-required-read "Project key" project-key-list))
+                    (key (jira-rest-required-read "Project key" project-key-list))
                     (project (find key projects :test 'equal :key 'project-key))
                     (issue-types (cdr (assoc 'issuetypes project)))
                     (issue-type-names (mapcar 'issue-type-name issue-types))
-                    (issue-type-name (completing-required-read "Issue type" issue-type-names))
+                    (issue-type-name (jira-rest-required-read "Issue type" issue-type-names))
                     (issue-type (find issue-type-name issue-types :test 'equal :key 'issue-type-name))
                     (project-id (cdr (assoc 'id project)))
                     (issue-type-id (cdr (assoc 'id issue-type)))
-                    (summary (do ((s (read-string "Summary: ") (read-string "Summary (required): ")))
-                                 ((not (equal s "")) s)))
+                    (summary (jira-rest-required-read "Summary"))
                     (description (read-string "Description: ")))
                ;; TODO: we should have a much more magit commit message style thing here
-               (jira-rest-post (jira-rest-create-issue-endpoint)
+               (jira-rest-post instance
+                               (jira-rest-create-issue-endpoint)
                                `((fields . ((project . ((id . ,project-id)))
                                             (issuetype . ((id . ,issue-type-id)))
                                             (summary . ,summary)
                                             ,@(unless (equal description "")
                                                 `((description . ,description))))))
-                               'got-new-issue-key)))
-           (got-new-issue-key (result)
+                               'got-new-issue-key
+                               (list instance))))
+           (got-new-issue-key (result instance)
              (let ((key (cdr (assoc 'key result))))
-               (jira-rest-show-issue key)
+               (jira-rest-show-issue instance key)
                (message "Created issue %s" key))))
-    (jira-rest-get (jira-rest-create-issue-metadata-endpoint)
-                   'got-projects-and-issues)))
+    (jira-rest-get instance
+                   (jira-rest-create-issue-metadata-endpoint)
+                   'got-projects-and-issues
+                   (list instance))))
 
-(defun jira-rest-revert-buffer (ignore-auto noconfirm)
-  "Reverts the buffer by repeating the request that generated.
+(defun jira-rest-revert-buffer (&optional ignore-auto noconfirm)
+  "Reverts a jira-rest buffer.
 
 Ignore the arguments as they don't really make sense for us."
-  (let ((command (jira-rest-history-current)))
-    (when command
-      (jira-rest-history-visit command))))
+  (apply 'funcall jira-rest-revert-buffer))
 
-(defun jira-rest-comment-issue (key comment)
+(defun jira-rest-comment-issue (instance key comment)
   "Adds a new comment with text COMMENT to the issue with the specified KEY."
-  (interactive (list (let ((command (jira-rest-history-current)))
-                       (if (eq (car command) 'jira-rest-show-issue)
-                           (cadr command)
-                         (do ((s (read-string "Issue key: ") (read-string "Issue key (required): ")))
-                             ((not (equal s "")) s))))
-                     (do ((s (read-string "Comment: ") (read-string "Comment (required): ")))
-                         ((not (equal s "")) s))))
-  (jira-rest-post (jira-rest-comment-endpoint key)
+  (interactive (list (jira-rest-infer-instance)
+                     (jira-rest-infer-issue)
+                     (jira-rest-required-read "Comment")))
+  (jira-rest-post instance
+                  (jira-rest-comment-endpoint key)
                   `((body . ,comment))
-                  (lambda (_ key)
-                    (jira-rest-show-issue key))
-                  (list key)))
+                  (lambda (_ instance key)
+                    (jira-rest-show-issue instance key))
+                  (list instance key)))
 
-(defun jira-rest-assign-issue (key assignee)
+(defun jira-rest-assign-issue (instance key assignee)
   "Assigns the issue with specified KEY to ASSIGNEE."
-  (interactive (let* ((key (let ((command (jira-rest-history-current)))
-                             (if (eq (car command) 'jira-rest-show-issue)
-                                 (cadr command)
-                               (do ((s (read-string "Issue key: ") (read-string "Issue key (required): ")))
-                                   ((not (equal s "")) s)))))
+  (interactive (let* ((instance (jira-rest-infer-instance))
+                      (key (jira-rest-infer-issue))
                       (completer (completion-table-dynamic
                                   (lambda (prefix)
-                                    (with-current-buffer (url-retrieve-synchronously
-                                                          (jira-rest-assignable-user-endpoint key prefix))
+                                    (with-current-buffer
+                                        (let ((url-request-extra-headers `(("Authorization" . ,(jira-rest-auth-token instance)))))
+                                          (url-retrieve-synchronously
+                                           (jira-rest-endpoint-for-instance instance (jira-rest-assignable-user-endpoint key prefix))))
                                       (with-jira-rest-response
                                        (jira-rest-skip-header)
                                        (let* ((names (mapcar (lambda (user) (cdr (assoc 'name user)))
                                             (and (<= (length prefix) (length name))
                                                  (string= prefix (subseq name 0 (length prefix)))))
                                           names))))))))
-                 (list key (completing-read "Assignee: " completer nil 'confirm))))
-  (jira-rest-put (jira-rest-issue-endpoint key)
+                 (list instance key (completing-read "Assignee: " completer nil 'confirm))))
+  (jira-rest-put instance
+                 (jira-rest-issue-endpoint key)
                  `((update . ((assignee . [((set . ((name . ,assignee))))]))))
-                 (lambda (_ key)
-                   (jira-rest-show-issue key))
-                 (list key)))
+                 (lambda (_ instance key)
+                   (jira-rest-show-issue instance key))
+                 (list instance key)))
 
-(defun jira-rest-update-issue-summary (key summary)
+(defun jira-rest-update-issue-summary (instance key summary)
   "Changes the summary of the issue with KEY to SUMMARY."
-  (interactive (list (let ((command (jira-rest-history-current)))
-                       (if (eq (car command) 'jira-rest-show-issue)
-                           (cadr command)
-                         (do ((s (read-string "Issue key: ") (read-string "Issue key (required): ")))
-                             ((not (equal s "")) s))))
-                     (do ((s (read-string "Summary: ") (read-string "Summary (required): ")))
-                         ((not (equal s "")) s))))
-  (jira-rest-put (jira-rest-issue-endpoint key)
+  (interactive (list (jira-rest-infer-instance)
+                     (jira-rest-infer-issue)
+                     (jira-rest-required-read "Summary")))
+  (jira-rest-put instance
+                 (jira-rest-issue-endpoint key)
                  `((update . ((summary . [((set . ,summary))]))))
-                 (lambda (_ key)
-                   (jira-rest-show-issue key))
-                 (list key)))
+                 (lambda (_ instance key)
+                   (jira-rest-show-issue instance key))
+                 (list instance key)))
 
 ;; TODO this and functions like this should accept transition names and transition ids
-(defun jira-rest-update-issue-status (key transition-name &optional resolution-name)
+(defun jira-rest-update-issue-status (instance key transition-name &optional resolution-name)
   "Changes the status of the issue with key KEY via the transition named by TRANSITION-NAME.
 
 If specified, the resolution for the issue is set to
 detect which transitions will require setting a resolution, and
 instead requires a resolution for the `Close Issue' and `Resolve
 Issue' transitions."
-  (interactive (let* ((key (let ((command (jira-rest-history-current)))
-                             (if (eq (car command) 'jira-rest-show-issue)
-                                 (cadr command)
-                               (do ((s (read-string "Issue key: ") (read-string "Issue key (required): ")))
-                                   ((not (equal s "")) s)))))
+  (interactive (let* ((instance (jira-rest-infer-instance))
+                      (key (jira-rest-infer-issue))
                       (transition-completer
                        (completion-table-dynamic
                         (lambda (prefix)
-                          (with-current-buffer (url-retrieve-synchronously
-                                                (jira-rest-issue-transitions-endpoint key))
+                          (with-current-buffer
+                              (let ((url-request-extra-headers `(("Authorization" . ,(jira-rest-auth-token instance)))))
+                                (url-retrieve-synchronously
+                                 (jira-rest-endpoint-for-instance instance (jira-rest-issue-transitions-endpoint key))))
                             (with-jira-rest-response
                              (jira-rest-skip-header)
                              (loop for transition in (cdr (assoc 'transitions
                                    if (and (<= (length prefix) (length name))
                                            (string= prefix (subseq name 0 (length prefix))))
                                    collect name))))))
-                      (transition-name
-                       (do ((s (completing-read "Transition: " transition-completer nil t)
-                               (completing-read "Transition (required):" transition-completer nil t)))
-                           ((not (equal s "")) s)))
+                      (transition-name (jira-rest-required-read "Transition" transition-completer))
                       (resolution
-                       (with-current-buffer (url-retrieve-synchronously
-                                             (jira-rest-issue-endpoint key))
+                       (with-current-buffer
+                           (let ((url-request-extra-headers `(("Authorization" . ,(jira-rest-auth-token instance)))))
+                             (url-retrieve-synchronously
+                              (jira-rest-endpoint-for-instance instance (jira-rest-issue-endpoint key))))
                          (with-jira-rest-response
                           (jira-rest-skip-header)
                           (cdr (assoc 'resolution (cdr (assoc 'fields (jira-rest-parse-document))))))))
                       (resolution-completer
                        (completion-table-dynamic
                         (lambda (prefix)
-                          (with-current-buffer (url-retrieve-synchronously
-                                                (jira-rest-resolutions-endpoint))
+                          (with-current-buffer
+                              (let ((url-request-extra-headers `(("Authorization" . ,(jira-rest-auth-token instance)))))
+                                (url-retrieve-synchronously
+                                 (jira-rest-endpoint-for-instance instance (jira-rest-resolutions-endpoint))))
                             (with-jira-rest-response
                              (jira-rest-skip-header)
                              (loop for resolution in (jira-rest-parse-document)
                                 ;; TODO Presumably there's a better way to do this but the REST
                                 ;; API doesn't make clear what it might be.
                                 (member transition-name '("Close Issue" "Resolve Issue")))
-                           (do ((s (completing-read "Resolution: " resolution-completer nil t)
-                                   (completing-read "Resolution (required): " resolution-completer nil t)))
-                               ((not (equal s "")) s))
+                           (jira-rest-required-read "Resolution" resolution-completer)
                          nil)))
-                 (list key transition-name resolution-name)))
-  (labels ((got-issue-transitions (transitions key transition-name resolution-name)
+                 (list instance key transition-name resolution-name)))
+  (labels ((got-issue-transitions (transitions instance key transition-name resolution-name)
              (let* ((transition (find transition-name (cdr (assoc 'transitions transitions))
                                       :test 'string=
                                       :key (lambda (transition) (cdr (assoc 'name transition)))))
                     (transition-id (cdr (assoc 'id transition))))
                (if resolution-name
-                   (jira-rest-get (jira-rest-resolutions-endpoint)
+                   (jira-rest-get instance
+                                  (jira-rest-resolutions-endpoint)
                                   'got-resolutions
-                                  (list key transition-id resolution-name))
-                 (send-post key `((transition . ((id . ,transition-id))))))))
-           (got-resolutions
-            (resolutions key transition-id resolution-name)
-            (let* ((resolution (find resolution-name resolutions
-                                     :test 'string=
-                                     :key (lambda (transition) (cdr (assoc 'name transition)))))
-                   (resolution-id (cdr (assoc 'id resolution))))
-              (send-post key `((transition . ((id . ,transition-id)))
-                               (fields . ((resolution . ((id . ,resolution-id)))))))))
-           (send-post
-            (key data)
-            (jira-rest-post (jira-rest-issue-transitions-endpoint key)
-                            data
-                            'got-post-response
-                            (list key)))
-           (got-post-response (_ key)
-             (jira-rest-show-issue key)))
-    (jira-rest-get (jira-rest-issue-transitions-endpoint key)
+                                  (list instance key transition-id resolution-name))
+                 (send-post instance key `((transition . ((id . ,transition-id))))))))
+           (got-resolutions (resolutions instance key transition-id resolution-name)
+             (let* ((resolution (find resolution-name resolutions
+                                      :test 'string=
+                                      :key (lambda (transition) (cdr (assoc 'name transition)))))
+                    (resolution-id (cdr (assoc 'id resolution))))
+               (send-post instance key `((transition . ((id . ,transition-id)))
+                                         (fields . ((resolution . ((id . ,resolution-id)))))))))
+           (send-post (instance key data)
+             (jira-rest-post instance
+                             (jira-rest-issue-transitions-endpoint key)
+                             data
+                             'got-post-response
+                             (list instance key)))
+           (got-post-response (_ instance key)
+             (jira-rest-show-issue instance key)))
+    (jira-rest-get instance
+                   (jira-rest-issue-transitions-endpoint key)
                    'got-issue-transitions
-                   (list key transition-name resolution-name))))
+                   (list instance key transition-name resolution-name))))
 
-(defun jira-rest-watch-issue (key &optional stop-watching)
+(defun jira-rest-watch-issue (instance key &optional stop-watching)
   "Starts watching the issue specified by KEY.
 
 If STOP-WATCHING is true (interactively, if this is called with a
 prefix arg), stops watching the issue instead."
-  (interactive (list (let ((command (jira-rest-history-current)))
-                       (if (eq (car command) 'jira-rest-show-issue)
-                           (cadr command)
-                         (do ((s (read-string "Issue key: ") (read-string "Issue key (required): ")))
-                             ((not (equal s "")) s))))
+  (interactive (list (jira-rest-infer-instance)
+                     (jira-rest-infer-issue)
                      current-prefix-arg))
-  (funcall (if stop-watching 'jira-rest-delete 'jira-rest-post)
-           (if stop-watching
-               (jira-rest-watchers-endpoint key (second jira-rest-instance))
-             (jira-rest-watchers-endpoint key))
-           (second jira-rest-instance)
-           (lambda (_ key)
-             (jira-rest-show-issue key))
-           (list key)))
-
-(defun jira-rest-send-region-as-comment (start end issue-key)
+  (let ((username (jira-rest-instance-username instance)))
+    (funcall (if stop-watching 'jira-rest-delete 'jira-rest-post)
+             instance
+             (if stop-watching
+                 (jira-rest-watchers-endpoint key username)
+               (jira-rest-watchers-endpoint key))
+             username
+             (lambda (_ instance key)
+               (jira-rest-show-issue instance key))
+             (list instance key))))
+
+(defun jira-rest-send-region-as-comment (start end instance issue-key)
   "Adds the contents of the buffer between START and END as a comment on ISSUE-KEY.
 
 Interactively, uses the current region."
-  (interactive "r\nsIssue key: ")
-  (jira-rest-comment-issue issue-key (buffer-substring start end)))
-
-
-;;; History stuff
-;; TODO: Is there a better way to do this? How does the help mode do it?
-
-(defvar *jira-rest-history-backward* nil
-  "Commands to execute to move backwards through history.")
-(defvar *jira-rest-history-current* nil
-  "Currently displayed command.")
-(defvar *jira-rest-history-forward* nil
-  "Commands to execute to move forward through history.")
-
-(defun jira-rest-history-back ()
-  "Move backward through the command history.
-
-Has no effect if there's no more history to move back through."
-  (interactive)
-  (when (first (jira-rest-has-history))
-    (push *jira-rest-history-current* *jira-rest-history-forward*)
-    (setf *jira-rest-history-current* (pop *jira-rest-history-backward*))
-    (jira-rest-history-visit *jira-rest-history-current*)))
-
-(defun jira-rest-history-forward ()
-  "Move forward through the command history.
-
-Has no effect if there's no more history to move forward through."
-  (interactive)
-  (when (second (jira-rest-has-history))
-    (push *jira-rest-history-current* *jira-rest-history-backward*)
-    (setf *jira-rest-history-current* (pop *jira-rest-history-forward*))
-    (jira-rest-history-visit *jira-rest-history-current*)))
-
-(defun jira-rest-history-reset ()
-  (setf *jira-rest-history-backward* nil
-        *jira-rest-history-current* nil
-        *jira-rest-history-forward* nil))
-
-(defun jira-rest-history-record (command &rest args)
-  (let ((new-command (cons command args)))
-    (unless (equal new-command (jira-rest-history-current))
-      (unless (null *jira-rest-history-current*)
-        (push *jira-rest-history-current* *jira-rest-history-backward*))
-      (setf *jira-rest-history-current* new-command
-            *jira-rest-history-forward* nil))))
-
-(defun jira-rest-history-current ()
-  *jira-rest-history-current*)
-
-(defun jira-rest-history-visit (command)
-  (apply (car command) (cdr command)))
-
-(defun jira-rest-has-history ()
-  "Returns a list of two elements: whether there are history elements backward/forward."
-  (list
-   (not (null *jira-rest-history-backward*))
-   (not (null *jira-rest-history-forward*))))
+  (interactive (list (region-beginning)
+                     (region-end)
+                     (jira-rest-infer-instance)
+                     (jira-rest-infer-issue)))
+  (jira-rest-comment-issue instance issue-key (buffer-substring start end)))
 
 
 ;;; Done
+Probably don't need to be different modes. Just different windows
+with commands that are smart enough to allow information to be
+inferred from e.g. thing at point and buffer-local
+`jira-rest-current-project' or whatever.
+
+-- This is what I've done. Issues have their own mode now as there are
+   keybindings that make sense there while not making sense in e.g. search
+   results (maybe).
+
+I CAN hook into thing-at-point! Returning nil if there's nothing
+there is totally kosher! These are functions that are
+hypothetically generally useful!
+
+-- I haven't done this and instance/issue/project inference doesn't exist
+   (mostly) and where it does doesn't use this.
+
+* earmuffs for jira-rest-instance - wontfix
+* improve rebinding of jira-rest-instance - wontfix
+* merge basic auth branch - done
+
+* make synchronous requests easier
+* fix interactive declarations
+* infer more interactive arguments from ambient state (including point)
+* more cleanly separate commands from the under-the-hood code
+* clean up code
+* check header comment is correct, add getting started information