diff options
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.el | 912 |
1 files changed, 0 insertions, 912 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 deleted file mode 100644 index 6d9ce8275576..000000000000 --- a/configs/shared/emacs/.emacs.d/elpa/ghub-20180911.1858/ghub.el +++ /dev/null @@ -1,912 +0,0 @@ -;;; 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 |