about summary refs log tree commit diff
path: root/configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el
diff options
context:
space:
mode:
Diffstat (limited to 'configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el')
-rw-r--r--configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el912
1 files changed, 912 insertions, 0 deletions
diff --git a/configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el b/configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el
new file mode 100644
index 000000000000..6d9ce8275576
--- /dev/null
+++ b/configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el
@@ -0,0 +1,912 @@
+;;; ghub.el --- minuscule client libraries for Git forge APIs  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2016-2018  Jonas Bernoulli
+
+;; Author: Jonas Bernoulli <jonas@bernoul.li>
+;; Homepage: https://github.com/magit/ghub
+;; Keywords: tools
+
+;; This file is not part of GNU Emacs.
+
+;; 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 3, 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.
+
+;; For a copy of the GPL see https://www.gnu.org/licenses/gpl.txt.
+
+;;; Commentary:
+
+;; Ghub provides basic support for using the APIs of various Git forges
+;; from Emacs packages.  Originally it only supported the Github REST
+;; API, but now it also supports the Github GraphQL API as well as the
+;; REST APIs of Gitlab, Gitea, Gogs and Bitbucket.
+
+;; Ghub abstracts access to API resources using only a handful of basic
+;; functions such as `ghub-get'.  These are convenience wrappers around
+;; `ghub-request'.  Additional forge-specific wrappers like `glab-put',
+;; `gtea-put', `gogs-post' and `buck-delete' are also available.  Ghub
+;; does not provide any resource-specific functions, with the exception
+;; of `FORGE-repository-id'.
+
+;; When accessing Github, then Ghub handles the creation and storage of
+;; access tokens using a setup wizard to make it easier for users to get
+;; started.  The tokens for other forges have to be created manually.
+
+;; Ghub is intentionally limited to only provide these two essential
+;; features — basic request functions and guided setup — to avoid being
+;; too opinionated, which would hinder wide adoption.  It is assumed that
+;; wide adoption would make life easier for users and maintainers alike,
+;; because then all packages that talk to forge APIs could be configured
+;; the same way.
+
+;; Please consult the manual (info "ghub") for more information.
+
+;;; Code:
+
+(require 'auth-source)
+(require 'cl-lib)
+(require 'json)
+(require 'let-alist)
+(require 'url)
+(require 'url-auth)
+(require 'url-http)
+
+(eval-when-compile (require 'subr-x))
+
+(defvar url-callback-arguments)
+(defvar url-http-end-of-headers)
+(defvar url-http-extra-headers)
+(defvar url-http-response-status)
+
+;;; Settings
+
+(defconst ghub-default-host "api.github.com"
+  "The default host that is used if `ghub.host' is not set.")
+
+(defvar ghub-github-token-scopes '(repo)
+  "The Github API scopes that your private tools need.
+
+The token that is created based on the value of this variable
+is used when `ghub-request' (or one of its wrappers) is called
+without providing a value for AUTH.  Packages should always
+identify themselves using that argument, but when you use Ghub
+directly in private tools, then that is not necessary and the
+request is made on behalf of the `ghub' package itself, aka on
+behalf of some private tool.
+
+By default the only requested scope is `repo' because that is
+sufficient as well as required for most common uses.  This and
+other scopes are documented at URL `https://magit.vc/goto/2e586d36'.
+
+If your private tools need other scopes, then you have to add
+them here *before* creating the token.  Alternatively you can
+edit the scopes of an existing token using the web interface
+at URL `https://github.com/settings/tokens'.")
+
+(defvar ghub-override-system-name nil
+  "If non-nil, the string used to identify the local machine.
+If this is nil, then the value returned by `system-name' is
+used instead.")
+
+;;; Request
+;;;; Object
+
+(cl-defstruct (ghub--req
+               (:constructor ghub--make-req)
+               (:copier nil))
+  (url        nil :read-only nil)
+  (forge      nil :read-only t)
+  (silent     nil :read-only t)
+  (method     nil :read-only t)
+  (headers    nil :read-only t)
+  (handler    nil :read-only t)
+  (unpaginate nil :read-only nil)
+  (noerror    nil :read-only t)
+  (reader     nil :read-only t)
+  (callback   nil :read-only t)
+  (errorback  nil :read-only t)
+  (value      nil :read-only nil)
+  (extra      nil :read-only nil))
+
+(defalias 'ghub-req-extra 'ghub--req-extra)
+
+;;;; API
+
+(define-error 'ghub-error "Ghub/Url Error" 'error)
+(define-error 'ghub-http-error "HTTP Error" 'ghub-error)
+
+(defvar ghub-response-headers nil
+  "The headers returned in response to the last request.
+`ghub-request' returns the response body and stores the
+response headers in this variable.")
+
+(cl-defun ghub-head (resource &optional params
+                              &key query payload headers
+                              silent unpaginate noerror reader
+                              username auth host
+                              callback errorback extra)
+  "Make a `HEAD' request for RESOURCE, with optional query PARAMS.
+Like calling `ghub-request' (which see) with \"HEAD\" as METHOD."
+  (ghub-request "HEAD" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-get (resource &optional params
+                             &key query payload headers
+                             silent unpaginate noerror reader
+                             username auth host
+                             callback errorback extra)
+  "Make a `GET' request for RESOURCE, with optional query PARAMS.
+Like calling `ghub-request' (which see) with \"GET\" as METHOD."
+  (ghub-request "GET" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-put (resource &optional params
+                             &key query payload headers
+                             silent unpaginate noerror reader
+                             username auth host
+                             callback errorback extra)
+  "Make a `PUT' request for RESOURCE, with optional payload PARAMS.
+Like calling `ghub-request' (which see) with \"PUT\" as METHOD."
+  (ghub-request "PUT" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-post (resource &optional params
+                              &key query payload headers
+                              silent unpaginate noerror reader
+                              username auth host
+                              callback errorback extra)
+  "Make a `POST' request for RESOURCE, with optional payload PARAMS.
+Like calling `ghub-request' (which see) with \"POST\" as METHOD."
+  (ghub-request "POST" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-patch (resource &optional params
+                               &key query payload headers
+                               silent unpaginate noerror reader
+                               username auth host
+                               callback errorback extra)
+  "Make a `PATCH' request for RESOURCE, with optional payload PARAMS.
+Like calling `ghub-request' (which see) with \"PATCH\" as METHOD."
+  (ghub-request "PATCH" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-delete (resource &optional params
+                                &key query payload headers
+                                silent unpaginate noerror reader
+                                username auth host
+                                callback errorback extra)
+  "Make a `DELETE' request for RESOURCE, with optional payload PARAMS.
+Like calling `ghub-request' (which see) with \"DELETE\" as METHOD."
+  (ghub-request "DELETE" resource params
+                :query query :payload payload :headers headers
+                :silent silent :unpaginate unpaginate
+                :noerror noerror :reader reader
+                :username username :auth auth :host host
+                :callback callback :errorback errorback :extra extra))
+
+(cl-defun ghub-request (method resource &optional params
+                               &key query payload headers
+                               silent unpaginate noerror reader
+                               username auth host forge
+                               callback errorback value extra)
+  "Make a request for RESOURCE and return the response body.
+
+Also place the response header in `ghub-response-headers'.
+
+METHOD is the HTTP method, given as a string.
+RESOURCE is the resource to access, given as a string beginning
+  with a slash.
+
+PARAMS, QUERY, PAYLOAD and HEADERS are alists used to specify
+  data.  The Github API documentation is vague on how data has
+  to be transmitted and for a particular resource usually just
+  talks about \"parameters\".  Generally speaking when the METHOD
+  is \"HEAD\" or \"GET\", then they have to be transmitted as a
+  query, otherwise as a payload.
+Use PARAMS to automatically transmit like QUERY or PAYLOAD would
+  depending on METHOD.
+Use QUERY to explicitly transmit data as a query.
+Use PAYLOAD to explicitly transmit data as a payload.
+  Instead of an alist, PAYLOAD may also be a string, in which
+  case it gets encoded as UTF-8 but is otherwise transmitted as-is.
+Use HEADERS for those rare resources that require that the data
+  is transmitted as headers instead of as a query or payload.
+  When that is the case, then the API documentation usually
+  mentions it explicitly.
+
+If SILENT is non-nil, then don't message progress reports and
+  the like.
+
+If UNPAGINATE is t, then make as many requests as necessary to
+  get all values.  If UNPAGINATE is a natural number, then get
+  at most that many pages.  For any other non-nil value raise
+  an error.
+If NOERROR is non-nil, then do not raise an error if the request
+  fails and return nil instead.  If NOERROR is `return', then
+  return the error payload instead of nil.
+If READER is non-nil, then it is used to read and return from the
+  response buffer.  The default is `ghub--read-json-payload'.
+  For the very few resources that do not return JSON, you might
+  want to use `ghub--decode-payload'.
+
+If USERNAME is non-nil, then make a request on behalf of that
+  user.  It is better to specify the user using the Git variable
+  `github.user' for \"api.github.com\", or `github.HOST.user' if
+  connecting to a Github Enterprise instance.
+
+Each package that uses `ghub' should use its own token. If AUTH
+  is nil, then the generic `ghub' token is used instead.  This
+  is only acceptable for personal utilities.  A packages that
+  is distributed to other users should always use this argument
+  to identify itself, using a symbol matching its name.
+
+  Package authors who find this inconvenient should write a
+  wrapper around this function and possibly for the
+  method-specific functions as well.
+
+  Some symbols have a special meaning.  `none' means to make an
+  unauthorized request.  `basic' means to make a password based
+  request.  If the value is a string, then it is assumed to be
+  a valid token.  `basic' and an explicit token string are only
+  intended for internal and debugging uses.
+
+  If AUTH is a package symbol, then the scopes are specified
+  using the variable `AUTH-github-token-scopes'.  It is an error
+  if that is not specified.  See `ghub-github-token-scopes' for
+  an example.
+
+If HOST is non-nil, then connect to that Github instance.  This
+  defaults to \"api.github.com\".  When a repository is connected
+  to a Github Enterprise instance, then it is better to specify
+  that using the Git variable `github.host' instead of using this
+  argument.
+
+If FORGE is `gitlab', then connect to Gitlab.com or, depending
+  on HOST, to another Gitlab instance.  This is only intended for
+  internal use.  Instead of using this argument you should use
+  function `glab-request' and other `glab-*' functions.
+
+If CALLBACK and/or ERRORBACK is non-nil, then make one or more
+  asynchronous requests and call CALLBACK or ERRORBACK when
+  finished.  If an error occurred, then call ERRORBACK, or if
+  that is nil, then CALLBACK.  When no error occurred then call
+  CALLBACK.  When making asynchronous requests, then no errors
+  are signaled, regardless of the value of NOERROR.
+
+Both callbacks are called with four arguments.
+  1. For CALLBACK, the combined value of the retrieved pages.
+     For ERRORBACK, the error that occured when retrieving the
+     last page.
+  2. The headers of the last page as an alist.
+  3. Status information provided by `url-retrieve'. Its `:error'
+     property holds the same information as ERRORBACK's first
+     argument.
+  4. A `ghub--req' struct, which can be passed to `ghub-continue'
+     (which see) to retrieve the next page, if any."
+  (cl-assert (or (booleanp unpaginate) (natnump unpaginate)))
+  (unless (string-prefix-p "/" resource)
+    (setq resource (concat "/" resource)))
+  (unless host
+    (setq host (ghub--host forge)))
+  (unless (or username (stringp auth) (eq auth 'none))
+    (setq username (ghub--username host forge)))
+  (cond ((not params))
+        ((member method '("GET" "HEAD"))
+         (when query
+           (error "PARAMS and QUERY are mutually exclusive for METHOD %S"
+                  method))
+         (setq query params))
+        (t
+         (when payload
+           (error "PARAMS and PAYLOAD are mutually exclusive for METHOD %S"
+                  method))
+         (setq payload params)))
+  (when (or callback errorback)
+    (setq noerror t))
+  (ghub--retrieve
+   (ghub--encode-payload payload)
+   (ghub--make-req
+    :url (url-generic-parse-url
+          (concat "https://" host resource
+                  (and query (concat "?" (ghub--url-encode-params query)))))
+    :forge forge
+    :silent silent
+    ;; Encode in case caller used (symbol-name 'GET). #35
+    :method     (encode-coding-string method 'utf-8)
+    :headers    (ghub--headers headers host auth username forge)
+    :handler    'ghub--handle-response
+    :unpaginate unpaginate
+    :noerror    noerror
+    :reader     reader
+    :callback   callback
+    :errorback  errorback
+    :value      value
+    :extra      extra)))
+
+(defun ghub-continue (req)
+  "If there is a next page, then retrieve that.
+
+This function is only intended to be called from callbacks.  If
+there is a next page, then retrieve that and return the buffer
+that the result will be loaded into, or t if the process has
+already completed.  If there is no next page, then return nil.
+
+Callbacks are called with four arguments (see `ghub-request').
+The forth argument is a `ghub--req' struct, intended to be passed
+to this function.  A callback may use the struct's `extra' slot
+to pass additional information to the callback that will be
+called after the next request has finished.  Use the function
+`ghub-req-extra' to get and set the value of this slot."
+  (and (assq 'next (ghub-response-link-relations req))
+       (or (ghub--retrieve nil req) t)))
+
+(cl-defun ghub-wait (resource &optional duration &key username auth host)
+  "Busy-wait up to DURATION seconds for RESOURCE to become available.
+
+DURATION specifies how many seconds to wait at most.  It defaults
+to 64 seconds.  The first attempt is made immediately, the second
+after two seconds, and each subsequent attempt is made after
+waiting as long again as we already waited between all preceding
+attempts combined.
+
+See `ghub-request' for information about the other arguments."
+  (unless duration
+    (setq duration 64))
+  (with-local-quit
+    (let ((total 0))
+      (while (not (ghub-get resource nil
+                            :noerror t
+                            :username username
+                            :auth auth
+                            :host host))
+        (message "Waited (%3ss of %ss) for %s..." total duration resource)
+        (if (= total duration)
+            (error "Github is taking too long to create %s" resource)
+          (if (> total 0)
+              (let ((wait (min total (- duration total))))
+                (sit-for wait)
+                (cl-incf total wait))
+            (sit-for (setq total 2))))))))
+
+(defun ghub-response-link-relations (req &optional headers payload)
+  "Return an alist of link relations in HEADERS.
+If optional HEADERS is nil, then return those that were
+previously stored in the variable `ghub-response-headers'.
+
+When accessing a Bitbucket instance then the link relations
+are in PAYLOAD instead of HEADERS, making their API merely
+RESTish and forcing this function to append those relations
+to the value of `ghub-response-headers', for later use when
+this function is called with nil for PAYLOAD."
+  (if (eq (ghub--req-forge req) 'bitbucket)
+      (if payload
+          (let* ((page (cl-mapcan (lambda (key)
+                                    (when-let ((elt (assq key payload)))
+                                      (list elt)))
+                                  '(size page pagelen next previous)))
+                 (headers (cons (cons 'link-alist page) headers)))
+            (if (and req (or (ghub--req-callback req)
+                             (ghub--req-errorback req)))
+                (setq-local ghub-response-headers headers)
+              (setq-default ghub-response-headers headers))
+            page)
+        (cdr (assq 'link-alist ghub-response-headers)))
+  (when-let ((rels (cdr (assoc "Link" (or headers ghub-response-headers)))))
+    (mapcar (lambda (elt)
+              (pcase-let ((`(,url ,rel) (split-string elt "; ")))
+                (cons (intern (substring rel 5 -1))
+                      (substring url 1 -1))))
+            (split-string rels ", ")))))
+
+;;;; Internal
+
+(cl-defun ghub--retrieve (payload req)
+  (let ((url-request-extra-headers
+         (let ((headers (ghub--req-headers req)))
+           (if (functionp headers) (funcall headers) headers)))
+        (url-request-method (ghub--req-method req))
+        (url-request-data payload)
+        (url-show-status nil)
+        (url     (ghub--req-url req))
+        (handler (ghub--req-handler req))
+        (silent  (ghub--req-silent req)))
+    (if (or (ghub--req-callback  req)
+            (ghub--req-errorback req))
+        (url-retrieve url handler (list req) silent)
+      ;; When this function has already been called, then it is a
+      ;; no-op.  Otherwise it sets `url-registered-auth-schemes' among
+      ;; other things.  If we didn't ensure that it has been run, then
+      ;; `url-retrieve-synchronously' would do it, which would cause
+      ;; the value that we let-bind below to be overwritten, and the
+      ;; "default" value to be lost outside the let-binding.
+      (url-do-setup)
+      (with-current-buffer
+          (let ((url-registered-auth-schemes
+                 '(("basic" ghub--basic-auth-errorback . 10))))
+            (url-retrieve-synchronously url silent))
+        (funcall handler (car url-callback-arguments) req)))))
+
+(defun ghub--handle-response (status req)
+  (let ((buffer (current-buffer)))
+    (unwind-protect
+        (progn
+          (set-buffer-multibyte t)
+          (let* ((unpaginate (ghub--req-unpaginate req))
+                 (headers    (ghub--handle-response-headers status req))
+                 (payload    (ghub--handle-response-payload req))
+                 (payload    (ghub--handle-response-error status payload req))
+                 (value      (ghub--handle-response-value payload req))
+                 (next       (cdr (assq 'next (ghub-response-link-relations
+                                               req headers payload)))))
+            (when (numberp unpaginate)
+              (cl-decf unpaginate))
+            (setf (ghub--req-url req)
+                  (url-generic-parse-url next))
+            (setf (ghub--req-unpaginate req) unpaginate)
+            (or (and next
+                     unpaginate
+                     (or (eq unpaginate t)
+                         (>  unpaginate 0))
+                     (ghub-continue req))
+                (let ((callback  (ghub--req-callback req))
+                      (errorback (ghub--req-errorback req))
+                      (err       (plist-get status :error)))
+                  (cond ((and err errorback)
+                         (funcall errorback err headers status req))
+                        (callback
+                         (funcall callback value headers status req))
+                        (t value))))))
+      (when (buffer-live-p buffer)
+        (kill-buffer buffer)))))
+
+(defun ghub--handle-response-headers (status req)
+  (goto-char (point-min))
+  (forward-line 1)
+  (let (headers)
+    (while (re-search-forward "^\\([^:]*\\): \\(.+\\)"
+                              url-http-end-of-headers t)
+      (push (cons (match-string 1)
+                  (match-string 2))
+            headers))
+    (setq headers (nreverse headers))
+    (unless url-http-end-of-headers
+      (error "BUG: missing headers %s" (plist-get status :error)))
+    (goto-char (1+ url-http-end-of-headers))
+    (if (and req (or (ghub--req-callback req)
+                     (ghub--req-errorback req)))
+        (setq-local ghub-response-headers headers)
+      (setq-default ghub-response-headers headers))
+    headers))
+
+(defun ghub--handle-response-error (status payload req)
+  (let ((noerror (ghub--req-noerror req))
+        (err (plist-get status :error)))
+    (if err
+        (if noerror
+            (if (eq noerror 'return)
+                payload
+              (setcdr (last err) (list payload))
+              nil)
+          (ghub--signal-error err payload))
+      payload)))
+
+(defun ghub--signal-error (err &optional payload)
+  (pcase-let ((`(,symb . ,data) err))
+    (if (eq symb 'error)
+        (if (eq (car-safe data) 'http)
+            (signal 'ghub-http-error
+                    (let ((code (car (cdr-safe data))))
+                      (list code
+                            (nth 2 (assq code url-http-codes))
+                            payload)))
+          (signal 'ghub-error data))
+      (signal symb data))))
+
+(defun ghub--handle-response-value (payload req)
+  (setf (ghub--req-value req)
+        (nconc (ghub--req-value req)
+               (if-let ((nested (and (eq (ghub--req-forge req) 'bitbucket)
+                                     (assq 'values payload))))
+                   (cdr nested)
+                 payload))))
+
+(defun ghub--handle-response-payload (req)
+  (funcall (or (ghub--req-reader req)
+               'ghub--read-json-payload)
+           url-http-response-status))
+
+(defun ghub--read-json-payload (_status)
+  (let ((raw (ghub--decode-payload)))
+    (and raw
+         (condition-case nil
+             (let ((json-object-type 'alist)
+                   (json-array-type  'list)
+                   (json-key-type    'symbol)
+                   (json-false       nil)
+                   (json-null        nil))
+               (json-read-from-string raw))
+           (json-readtable-error
+            `((message
+               . ,(if (looking-at "<!DOCTYPE html>")
+                      (if (re-search-forward
+                           "<p>\\(?:<strong>\\)?\\([^<]+\\)" nil t)
+                          (match-string 1)
+                        "error description missing")
+                    (string-trim (buffer-substring (point) (point-max)))))
+              (documentation_url
+               . "https://github.com/magit/ghub/wiki/Github-Errors")))))))
+
+(defun ghub--decode-payload (&optional _status)
+  (and (not (eobp))
+       (decode-coding-string
+        (buffer-substring-no-properties (point) (point-max))
+        'utf-8)))
+
+(defun ghub--encode-payload (payload)
+  (and payload
+       (progn
+         (unless (stringp payload)
+           (setq payload (json-encode-list payload)))
+         (encode-coding-string payload 'utf-8))))
+
+(defun ghub--url-encode-params (params)
+  (mapconcat (lambda (param)
+               (pcase-let ((`(,key . ,val) param))
+                 (concat (url-hexify-string (symbol-name key)) "="
+                         (if (integerp val)
+                             (number-to-string val)
+                           (url-hexify-string val)))))
+             params "&"))
+
+;;; Authentication
+;;;; API
+
+;;;###autoload
+(defun ghub-create-token (host username package scopes)
+  "Create, store and return a new token.
+
+HOST is the Github instance, usually \"api.github.com\".
+USERNAME is the name of a user on that instance.
+PACKAGE is the package that will use the token.
+SCOPES are the scopes the token is given access to."
+  (interactive
+   (pcase-let ((`(,host ,username ,package)
+                (ghub--read-triplet)))
+     (list host username package
+           (split-string
+            (read-string
+             "Scopes (separated by commas): "
+             (mapconcat #'symbol-name
+                        (symbol-value
+                         (intern (format "%s-github-token-scopes" package)))
+                        ","))
+            "," t "[\s\t]+"))))
+  (let ((user (ghub--ident username package)))
+    (cl-destructuring-bind (save token)
+        (ghub--auth-source-get (list :save-function :secret)
+          :create t :host host :user user
+          :secret
+          (cdr (assq 'token
+                     (ghub-post
+                      "/authorizations"
+                      `((scopes . ,scopes)
+                        (note   . ,(ghub--ident-github package)))
+                      :username username :auth 'basic :host host))))
+      ;; Build-in back-ends return a function that does the actual
+      ;; saving, while for some third-party back-ends ":create t"
+      ;; is enough.
+      (when (functionp save)
+        (funcall save))
+      ;; If the Auth-Source cache contains the information that there
+      ;; is no value, then setting the value does not invalidate that
+      ;; now incorrect information.
+      (auth-source-forget (list :host host :user user))
+      token)))
+
+;;;###autoload
+(defun ghub-token-scopes (host username package)
+  "Return and echo the scopes of the specified token.
+This is intended for debugging purposes only.  The user
+has to provide several values including their password."
+  (interactive (ghub--read-triplet))
+  (let ((scopes
+         (cdr (assq 'scopes (ghub--get-token-plist host username package)))))
+    (when (called-interactively-p 'any)
+      ;; Also show the input values to make it easy for package
+      ;; authors to verify that the user has done it correctly.
+      (message "Scopes for %s@%s: %S"
+               (ghub--ident username package)
+               host scopes))
+    scopes))
+
+;;;###autoload
+(defun ghub-clear-caches ()
+  "Clear all caches that might negatively affect Ghub.
+
+If a library that is used by Ghub caches incorrect information
+such as a mistyped password, then that can prevent Ghub from
+asking the user for the correct information again.
+
+Set `url-http-real-basic-auth-storage' to nil
+and call `auth-source-forget+'."
+  (interactive)
+  (setq url-http-real-basic-auth-storage nil)
+  (auth-source-forget+))
+
+;;;; Internal
+
+(defun ghub--headers (headers host auth username forge)
+  (push (cons "Content-Type" "application/json") headers)
+  (if (eq auth 'none)
+      headers
+    (unless (or username (stringp auth))
+      (setq username (ghub--username host forge)))
+    (lambda ()
+      (if (eq auth 'basic)
+          (cons (cons "Authorization" (ghub--basic-auth host username))
+                headers)
+        (cons (ghub--auth host auth username forge) headers)))))
+
+(defun ghub--auth (host auth &optional username forge)
+  (unless username
+    (setq username (ghub--username host)))
+  (if (eq auth 'basic)
+      (cl-ecase forge
+        ((nil github gitea gogs bitbucket)
+         (cons "Authorization" (ghub--basic-auth host username)))
+        (gitlab
+         (error "Gitlab does not support basic authentication")))
+    (cons (cl-ecase forge
+            ((nil github gitea gogs bitbucket)
+             "Authorization")
+            (gitlab
+             "Private-Token"))
+          (concat
+           (and (not (eq forge 'gitlab)) "token ")
+           (encode-coding-string
+            (cl-typecase auth
+              (string auth)
+              (null   (ghub--token host username 'ghub nil forge))
+              (symbol (ghub--token host username auth  nil forge))
+              (t (signal 'wrong-type-argument
+                         `((or stringp symbolp) ,auth))))
+            'utf-8)))))
+
+(defun ghub--basic-auth (host username)
+  (let ((url (url-generic-parse-url (concat "https://" host))))
+    (setf (url-user url) username)
+    (url-basic-auth url t)))
+
+(defun ghub--basic-auth-errorback (url &optional prompt _overwrite _realm _args)
+  ;; This gets called twice.  Do nothing the first time,
+  ;; when PROMPT is nil.  See `url-get-authentication'.
+  (when prompt
+    (if (assoc "X-GitHub-OTP" (ghub--handle-response-headers nil nil))
+        (progn
+          (setq url-http-extra-headers
+                `(("Content-Type" . "application/json")
+                  ("X-GitHub-OTP" . ,(ghub--read-2fa-code))
+                  ;; Without "Content-Type" and "Authorization".
+                  ;; The latter gets re-added from the return value.
+                  ,@(cddr url-http-extra-headers)))
+          ;; Return the cached values, they are correct.
+          (url-basic-auth url nil nil nil))
+      ;; Remove the invalid cached values and fail, which
+      ;; is better than the invalid values sticking around.
+      (setq url-http-real-basic-auth-storage
+            (cl-delete (format "%s:%d" (url-host url) (url-port url))
+                       url-http-real-basic-auth-storage
+                       :test #'equal :key #'car))
+      nil)))
+
+(defun ghub--token (host username package &optional nocreate forge)
+  (let* ((user (ghub--ident username package))
+         (token
+          (or (car (ghub--auth-source-get (list :secret)
+                     :host host :user user))
+              (progn
+                ;; Auth-Source caches the information that there is no
+                ;; value, but in our case that is a situation that needs
+                ;; fixing so we want to keep trying by invalidating that
+                ;; information.
+                ;; The (:max 1) is needed and has to be placed at the
+                ;; end for Emacs releases before 26.1.  See #24, #64.
+                (auth-source-forget (list :host host :user user :max 1))
+                (and (not nocreate)
+                     (cl-ecase forge
+                       ((nil github)
+                        (ghub--confirm-create-token host username package))
+                       ((gitlab gitea gogs bitbucket)
+                        (error "Required %s token (%S for %S) does not exist.
+See https://magit.vc/manual/ghub/Support-for-Other-Forges.html for instructions."
+                               (capitalize (symbol-name forge))
+                               user host))))))))
+    (if (functionp token) (funcall token) token)))
+
+(defun ghub--host (&optional forge)
+  (cl-ecase forge
+    ((nil github)
+     (or (ignore-errors (car (process-lines "git" "config" "github.host")))
+         ghub-default-host))
+    (gitlab
+     (or (ignore-errors (car (process-lines "git" "config" "gitlab.host")))
+         (bound-and-true-p glab-default-host)))
+    (gitea
+     (or (ignore-errors (car (process-lines "git" "config" "gitea.host")))
+         (bound-and-true-p gtea-default-host)))
+    (gogs
+     (or (ignore-errors (car (process-lines "git" "config" "gogs.host")))
+         (bound-and-true-p gogs-default-host)))
+    (bitbucket
+     (or (ignore-errors (car (process-lines "git" "config" "bitbucket.host")))
+         (bound-and-true-p buck-default-host)))))
+
+(defun ghub--username (host &optional forge)
+  (let ((var (cond ((equal host ghub-default-host)
+                    "github.user")
+                   ((equal host (bound-and-true-p glab-default-host))
+                    "gitlab.user")
+                   ((equal host (bound-and-true-p buck-default-host))
+                    "bitbucket.user")
+                   ((eq forge 'github)    (format "github.%s.user"    host))
+                   ((eq forge 'gitlab)    (format "gitlab.%s.user"    host))
+                   ((eq forge 'bitbucket) (format "bitbucket.%s.user" host))
+                   ((eq forge 'gitea)     (format "gitea.%s.user"     host))
+                   ((eq forge 'gogs)      (format "gogs.%s.user"      host)))))
+    (condition-case nil
+        (car (process-lines "git" "config" var))
+      (error
+       (let ((user (read-string
+                    (format "Git variable `%s' is unset.  Set to: " var))))
+         (or (and user (progn (call-process "git" nil nil nil
+                                            "config" "--global" var user)
+                              user))
+             (user-error "Abort")))))))
+
+(defun ghub--ident (username package)
+  (format "%s^%s" username package))
+
+(defun ghub--ident-github (package)
+  (format "Emacs package %s @ %s"
+          package
+          (or ghub-override-system-name (system-name))))
+
+(defun ghub--package-scopes (package)
+  (let ((var (intern (format "%s-github-token-scopes" package))))
+    (if (boundp var)
+        (symbol-value var)
+      (error "%s fails to define %s" package var))))
+
+(defun ghub--confirm-create-token (host username package)
+  (let* ((ident (ghub--ident-github package))
+         (scopes (ghub--package-scopes package))
+         (max-mini-window-height 40))
+    (if (let ((message-log-max nil))
+          (yes-or-no-p
+           (format
+            "Such a Github API token is not available:
+
+  Host:    %s
+  User:    %s
+  Package: %s
+
+  Scopes requested in `%s-github-token-scopes':\n%s
+  Store on Github as:\n    %S
+  Store locally according to option `auth-sources':\n    %S
+%s
+If in doubt, then abort and first view the section of the Ghub
+documentation called \"Manually Creating and Storing a Token\".
+
+Otherwise confirm and then provide your Github username and
+password at the next two prompts.  Depending on the backend
+you might have to provide a passphrase and confirm that you
+really want to save the token.
+
+Create and store such a token? "
+            host username package package
+            (mapconcat (lambda (scope) (format "    %s" scope)) scopes "\n")
+            ident auth-sources
+            (if (and (stringp (car auth-sources))
+                     (not (string-suffix-p ".gpg" (car auth-sources))))
+                (format "
+WARNING: The token will be stored unencrypted in %S.
+         If you don't want that, you have to abort and customize
+         the `auth-sources' option.\n" (car auth-sources))
+              ""))))
+        (progn
+          (when (ghub--get-token-id host username package)
+            (if (yes-or-no-p
+                 (format
+                  "A token named %S\nalready exists on Github.  Replace it?"
+                  ident))
+                (ghub--delete-token host username package)
+              (user-error "Abort")))
+          (ghub-create-token host username package scopes))
+      (user-error "Abort"))))
+
+(defun ghub--get-token-id (host username package)
+  (let ((ident (ghub--ident-github package)))
+    (cl-some (lambda (x)
+               (let-alist x
+                 (and (equal .app.name ident) .id)))
+             (ghub-get "/authorizations"
+                       '((per_page . 100))
+                       :unpaginate t
+                       :username username :auth 'basic :host host))))
+
+(defun ghub--get-token-plist (host username package)
+  (ghub-get (format "/authorizations/%s"
+                    (ghub--get-token-id host username package))
+            nil :username username :auth 'basic :host host))
+
+(defun ghub--delete-token (host username package)
+  (ghub-delete (format "/authorizations/%s"
+                       (ghub--get-token-id host username package))
+               nil :username username :auth 'basic :host host))
+
+(defun ghub--read-triplet ()
+  (let ((host (read-string "Host: " (ghub--host))))
+    (list host
+          (read-string "Username: " (ghub--username host))
+          (intern (read-string "Package: " "ghub")))))
+
+(defvar ghub--2fa-cache nil)
+
+(defun ghub--read-2fa-code ()
+  (let ((code (read-number "Two-factor authentication code: "
+                           (and ghub--2fa-cache
+                                (< (float-time (time-subtract
+                                                (current-time)
+                                                (cdr ghub--2fa-cache)))
+                                   25)
+                                (car ghub--2fa-cache)))))
+    (setq ghub--2fa-cache (cons code (current-time)))
+    (number-to-string code)))
+
+(defun ghub--auth-source-get (keys &rest spec)
+  (declare (indent 1))
+  (let ((plist (car (apply #'auth-source-search
+                           (append spec (list :max 1))))))
+    (mapcar (lambda (k)
+              (plist-get plist k))
+            keys)))
+
+(advice-add 'auth-source-netrc-parse-next-interesting :around
+            'auth-source-netrc-parse-next-interesting@save-match-data)
+(defun auth-source-netrc-parse-next-interesting@save-match-data (fn)
+  "Save match-data for the benefit of caller `auth-source-netrc-parse-one'.
+Without wrapping this function in `save-match-data' the caller
+won't see the secret from a line that is followed by a commented
+line."
+  (save-match-data (funcall fn)))
+
+;;; _
+(provide 'ghub)
+(require 'ghub-graphql)
+;;; ghub.el ends here