├── .gitignore ├── .travis.yml ├── Makefile ├── README.org ├── ghub+.el └── test ├── ert-tests.el └── linter.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | /.emake/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: required 3 | dist: trusty 4 | 5 | cache: 6 | - directories: 7 | - "$HOME/emacs" 8 | 9 | matrix: 10 | fast_finish: true 11 | allow_failures: 12 | - env: EMACS_VERSION=snapshot 13 | env: 14 | global: 15 | - EMAKE_SHA1=1b23379eb5a9f82d3e2d227d0f217864e40f23e0 16 | matrix: 17 | - EMACS_VERSION=25.1 18 | - EMACS_VERSION=25.2 19 | - EMACS_VERSION=25.3 20 | - EMACS_VERSION=26.1 21 | - EMACS_VERSION=26.1 MELPA_STABLE=true 22 | - EMACS_VERSION=snapshot 23 | 24 | before_install: 25 | - wget "https://raw.githubusercontent.com/vermiculus/emake.el/${EMAKE_SHA1}/emake.mk" 26 | - make setup 27 | 28 | install: 29 | - make install 30 | 31 | script: 32 | - make compile 33 | - make test-ert 34 | 35 | notifications: 36 | email: 37 | on_success: never 38 | on_failure: change 39 | webhooks: 40 | on_success: change 41 | on_failure: change 42 | on_start: never 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EMAKE_SHA1 = 1b23379eb5a9f82d3e2d227d0f217864e40f23e0 2 | EMACS_ARGS = --eval "(setq checkdoc-arguments-in-order-flag nil)" 3 | PACKAGE_BASENAME = ghub+ 4 | 5 | ifeq ($(MELPA_STABLE),true) 6 | PACKAGE_ARCHIVES = gnu melpa-stable 7 | else 8 | PACKAGE_ARCHIVES = gnu melpa 9 | endif 10 | PACKAGE_TEST_DEPS = dash s 11 | PACKAGE_TEST_ARCHIVES = gnu melpa 12 | 13 | include emake.mk 14 | 15 | .PHONY: clean 16 | 17 | clean: ## Clean generated files 18 | rm -rf $(EMAKE_WORKDIR) 19 | rm *.elc 20 | 21 | test-ert: EMACS_ARGS = -L ./test/ 22 | #test: test-ert 23 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+Title: GHub+ [[https://travis-ci.org/vermiculus/ghub-plus][https://travis-ci.org/vermiculus/ghub-plus.svg?branch=master]] [[https://melpa.org/#/ghub%2B][file:https://melpa.org/packages/ghub+-badge.svg]] 2 | 3 | GHub+ is a thick GitHub API client built using =API-Wrap.el= on =ghub=, 4 | [[https://github.com/tarsius/ghub][the minuscule GitHub API client]]. 5 | 6 | * Tour by Example 7 | #+BEGIN_SRC elisp 8 | ;;; GET /issues 9 | (ghubp-get-issues) 10 | 11 | ;;; GET /issues?state=closed 12 | (ghubp-get-issues :state 'closed) 13 | 14 | (let ((repo (ghub-get "/repos/magit/magit"))) 15 | (list 16 | ;; Magit's issues 17 | ;; GET /repos/magit/magit/issues 18 | (ghubp-get-repos-owner-repo-issues repo) 19 | 20 | ;; Magit's closed issues labeled 'easy' 21 | ;; GET /repos/magit/magit/issues?state=closed&labels=easy 22 | (ghubp-get-repos-owner-repo-issues repo 23 | :state 'closed :labels "easy"))) 24 | #+END_SRC 25 | 26 | * Contributing 27 | :PROPERTIES: 28 | :ID: 1F4644C5-72AC-49DA-A83C-443AA7F9651E 29 | :END: 30 | Contributions should be made via pull-request. When it makes sense, 31 | be sure your addition works when passing around object-alists. 32 | 33 | * Introduction 34 | This package is a thick client built on =ghub=, the miniscule GitHub 35 | client. Its aim is to provide the common functionality most helpful 36 | for application development. 37 | 38 | Since =ghub+= is built on =ghub=, any and all features you find lacking in 39 | =ghub+= can be done with =ghub= without needing to dig into either 40 | package's internals. However, =ghub+= provides some macros you may find 41 | helpful in development; see [[id:7208D9BD-1524-4701-A061-70861C5376DA][Extending]] for details. If you find your 42 | function to be particularly helpful or believe it to be a common use 43 | case, please consider [[id:1F4644C5-72AC-49DA-A83C-443AA7F9651E][contributing]] it to the library! 44 | 45 | * Extending 46 | :PROPERTIES: 47 | :ID: 7208D9BD-1524-4701-A061-70861C5376DA 48 | :END: 49 | To simplify application development, tools have been developed to 50 | shorten repetitive syntax and provide useful syntax for common 51 | problems that might not otherwise have good, succinct solutions. 52 | 53 | ** ~(ghubp-unpaginate &rest BODY)~ 54 | Wraps the form in a let-binding where ~ghub-unpaginate~ is ~t~. Forms 55 | executed here will continue to poll the API until all output has been 56 | received. 57 | 58 | ** ~(defapi{get,put,head,post,patch,delete}-ghubp ...)~ 59 | These wonderful macros super-charge the standard ~ghub-{get,put,...}~ 60 | functions into documentation-generating, resource-wrapping machines. 61 | Refer to their documentation or see [[https://github.com/vermiculus/apiwrap.el#using-the-generated-macros][apiwrap.el]] for a short tutorial in 62 | using these macros. 63 | # Local Variables: 64 | # org-id-link-to-org-use-id: t 65 | # End: 66 | -------------------------------------------------------------------------------- /ghub+.el: -------------------------------------------------------------------------------- 1 | ;;; ghub+.el --- a thick GitHub API client built on ghub -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2017-2018 Sean Allred 4 | 5 | ;; Author: Sean Allred 6 | ;; Keywords: extensions, multimedia, tools 7 | ;; Homepage: https://github.com/vermiculus/ghub-plus 8 | ;; Package-Requires: ((emacs "25") (ghub "2.0") (apiwrap "0.5")) 9 | ;; Package-Version: 0.4 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; Provides some sugar for `ghub'. See package `apiwrap' for 27 | ;; generated function usage instructions. 28 | 29 | ;;; Code: 30 | 31 | (require 'url) 32 | (require 'cl-lib) 33 | (require 'subr-x) 34 | (require 'ghub) 35 | (require 'apiwrap) 36 | 37 | (eval-and-compile 38 | (defun ghubp--make-link (alist) 39 | "Create a link from an ALIST of API endpoint properties." 40 | (format "https://developer.github.com/v3/%s" (alist-get 'link alist))) 41 | 42 | (defun ghubp--stringify-params (params) 43 | "Process PARAMS from textual data to Lisp structures." 44 | (mapcar (lambda (p) 45 | (if (listp p) 46 | (let ((k (car p)) (v (cdr p))) 47 | (cons k (alist-get v '((t . "true") (nil . "false")) v))) 48 | p)) 49 | params)) 50 | 51 | (defun ghubp--pre-process-params (params) 52 | (ghubp--stringify-params params)) 53 | 54 | (defvar ghubp-contextualize-function nil 55 | "Function to contextualize `ghub' requests. 56 | Can return an alist with any of the following properties: 57 | 58 | * `auth' 59 | * `headers' 60 | * `host' 61 | * `unpaginate' 62 | * `username' 63 | 64 | If (and only if) these properties are non-nil, they will provide 65 | values for the eponymous `ghub-request' keyword arguments. 66 | 67 | The function should be callable with no arguments. 68 | 69 | See also `ghubp-get-context'.") 70 | 71 | (defvar ghubp-request-override-function nil 72 | "Function to use instead of `ghub-request' for base calls. 73 | It is expected to have the same signature as `ghub-request'.") 74 | 75 | (defun ghubp-get-context () 76 | "Get the current context with `ghubp-contextualize-function'." 77 | (when (functionp ghubp-contextualize-function) 78 | (funcall ghubp-contextualize-function))) 79 | 80 | (defun ghubp-get-in-all (props object-list) 81 | "Follow property-path PROPS in OBJECT-LIST. 82 | Returns a list of the property-values." 83 | (declare (indent 1)) 84 | (if (or (null props) (not (consp props))) 85 | object-list 86 | (ghubp-get-in-all (cdr props) 87 | (mapcar (lambda (o) (alist-get (car props) o)) 88 | object-list)))) 89 | 90 | (defun ghubp-request (method resource params data) 91 | "Using METHOD, get RESOURCE with PARAMS and DATA. 92 | 93 | `ghubp-contextualize-function' is used to contextualize this 94 | request. 95 | 96 | If non-nil, `ghubp-request-override-function' is used instead of 97 | `ghub-request'. 98 | 99 | METHOD is one of `get', `put', `post', `head', `patch', and 100 | `delete'. 101 | 102 | RESOURCE is a string. 103 | 104 | PARAMS is a plist. 105 | 106 | DATA is an alist." 107 | (let-alist (ghubp-get-context) 108 | (let ((method (encode-coding-string (upcase (symbol-name method)) 'utf-8)) 109 | (params (apiwrap-plist->alist params))) 110 | (funcall (or ghubp-request-override-function 111 | #'ghub-request) 112 | method resource nil 113 | :query params 114 | :payload data 115 | :unpaginate .unpaginate 116 | :headers .headers 117 | :username .username 118 | :auth .auth 119 | :host .host)))) 120 | 121 | ;; If ghub-404 is not defined as an error, define it. 122 | ;; This will be necessary until Ghub releases v2. 123 | ;; See also #8. 124 | (unless (get 'ghub-404 'error-conditions) 125 | (define-error 'ghub-404 "Not Found" 'ghub-http-error)) 126 | 127 | (defun ghubp--catch (error-symbol &rest handlers) 128 | "Catch some Ghub signals as ERROR-SYMBOL with HANDLERS. 129 | Each element of HANDLERS should be a list of 130 | 131 | (HTTP-CODE HANDLER) 132 | 133 | where HTTP-CODE is an error code like 404. 134 | 135 | For use inside `:condition-case' endpoint configurations. 136 | 137 | See also `ghubp-catch' and `ghubp-catch*'." 138 | `((ghub-http-error 139 | (pcase (cadr ,error-symbol) 140 | ,@handlers 141 | (_ (signal (car ,error-symbol) (cdr ,error-symbol))))))) 142 | 143 | (defmacro ghubp-catch* (&rest handlers) 144 | "Catch some Ghub signals with HANDLERS. 145 | For use inside `:condition-case' endpoint configurations. 146 | 147 | For advanced error handling, the error is bound to the symbol `it'. 148 | 149 | See `ghubp--catch'." 150 | (apply #'ghubp--catch 'it handlers)) 151 | 152 | (defmacro ghubp-catch (error-symbol form &rest handlers) 153 | "Catch some Ghub signals as ERROR_SYMBOL in FORM with HANDLERS. 154 | For general use. 155 | 156 | See `ghubp--catch'" 157 | (declare (indent 2)) 158 | (when (eq error-symbol '_) 159 | (setq error-symbol (cl-gensym))) 160 | `(condition-case ,error-symbol ,form 161 | ,@(apply #'ghubp--catch error-symbol handlers))) 162 | 163 | (apiwrap-new-backend "GitHub" "ghubp" 164 | '((repo . "REPO is a repository alist of the form returned by `ghubp-get-user-repos'.") 165 | (branch . "BRANCH is a branch object of the form returned by `ghubp-get-repos-owner-repo-branches-branch'.") 166 | (org . "ORG is an organization alist of the form returned by `ghubp-get-user-orgs'.") 167 | (thread . "THREAD is a thread object of the form returned by `ghubp-get-repos-owner-repo-comments'.") 168 | (issue . "ISSUE is an issue object of the form returned by `ghubp-get-issues'.") 169 | (pull-request . "PULL-REQUEST is a pull request object of the form returned by `ghubp-get-repos-owner-repo-pulls'.") 170 | (review . "REVIEW is a review object of the form returned by `ghubp-get-repos-owner-repo-pulls-number-reviews'.") 171 | (label . "LABEL is a label object of the form returned by `ghubp-get-repos-owner-repo-issues-number-labels'.") 172 | (ref . "REF is a string and can be a SHA, a branch name, or a tag name.") 173 | (milestone . "MILESTONE is a milestone object.") 174 | (user . "USER is a user object.") 175 | (user-1 . "USER-1 is a user object.") 176 | (user-2 . "USER-2 is a user object.") 177 | (key . "KEY is a key object.")) 178 | :request #'ghubp-request 179 | :link #'ghubp--make-link 180 | :pre-process-params #'ghubp--pre-process-params)) 181 | 182 | 183 | ;;; Utilities: 184 | 185 | (defmacro ghubp-unpaginate (&rest body) 186 | "Unpaginate API responses while executing BODY." 187 | `(ghubp-override-context unpaginate t ,@body)) 188 | 189 | (defmacro ghubp-override-context (context new-value &rest body) 190 | "Execute BODY while manually overriding CONTEXT with NEW-VALUE. 191 | NEW-VALUE takes precedence over anything that 192 | `ghubp-contextualize-function' provides for CONTEXT, but 193 | `ghubp-contextualize-function' is otherwise respected." 194 | (declare (indent 2)) 195 | (unless (memq context '(host auth username unpaginate headers)) 196 | (error (concat "`ghubp-override-context' should only override one " 197 | "of the symbols from `ghubp-contextualize-function'."))) 198 | (let ((sym-other-context (cl-gensym))) 199 | `(let ((,sym-other-context (ghubp-get-context)) 200 | ghubp-contextualize-function) 201 | ;; override any existing value for CONTEXT 202 | (push (cons ',context ,new-value) ,sym-other-context) 203 | ;; and box the whole thing back into the var 204 | (setq ghubp-contextualize-function (lambda () ,sym-other-context)) 205 | ,@body))) 206 | 207 | (defun ghubp-keep-only (structure object) 208 | "Keep a specific STRUCTURE in OBJECT. 209 | See URL `http://emacs.stackexchange.com/a/31050/2264'." 210 | (declare (indent 1)) 211 | (if (and (consp object) (consp (car object)) (consp (caar object))) 212 | (mapcar (apply-partially #'ghubp-keep-only structure) object) 213 | (mapcar (lambda (el) 214 | (if (consp el) 215 | (cons (car el) 216 | (ghubp-keep-only (cdr el) (alist-get (car el) object))) 217 | (cons el (alist-get el object)))) 218 | structure))) 219 | 220 | (defun ghubp-header (header) 221 | "Get the value of HEADER from the last request as a string." 222 | (cdr (assoc-string header ghub-response-headers))) 223 | 224 | (defun ghubp-ratelimit-reset-time () 225 | "Get the reset time for the rate-limit as a time object." 226 | (declare (obsolete 'ghubp-ratelimit "2017-10-17")) 227 | (alist-get 'reset (ghubp-ratelimit))) 228 | 229 | (defun ghubp-ratelimit-remaining () 230 | "Get the remaining number of requests available." 231 | (declare (obsolete 'ghubp-ratelimit "2017-10-17")) 232 | (alist-get 'remaining (ghubp-ratelimit))) 233 | 234 | (defun ghubp-ratelimit (&optional no-headers) 235 | "Get `/rate_limit.rate'. 236 | Returns nil if the service is not rate-limited. Otherwise, 237 | returns an alist with the following properties: 238 | 239 | `.limit' 240 | number of requests we're allowed to make per hour. 241 | 242 | `.remaining' 243 | number of requests remaining for this hour. 244 | 245 | `.reset' 246 | time value of instant `.remaining' resets to `.limit'. 247 | 248 | Unless NO-HEADERS is non-nil, tries to use response headers 249 | instead of actually hitting /rate_limit." 250 | ;; todo: bug when headers are from other host 251 | (if (and (not no-headers) 252 | ghub-response-headers 253 | (assoc-string "X-RateLimit-Limit" ghub-response-headers)) 254 | (let* ((headers (list "X-RateLimit-Limit" "X-RateLimit-Remaining" "X-RateLimit-Reset")) 255 | (headers (mapcar (lambda (x) (string-to-number (ghubp-header x))) headers))) 256 | `((limit . ,(nth 0 headers)) 257 | (remaining . ,(nth 1 headers)) 258 | (reset . ,(seconds-to-time 259 | (nth 2 headers))))) 260 | (ghubp-catch _ 261 | (let-alist (ghubp-request 'get "/rate_limit" nil nil) 262 | .resources.core) 263 | ;; Enterprise returns 404 if rate limiting is disabled 264 | (404 nil)))) 265 | 266 | (defun ghubp--follow (method resource &optional params data) 267 | "Using METHOD, follow the RESOURCE link with PARAMS and DATA. 268 | This method is intended for use with callbacks." 269 | (let ((url (url-generic-parse-url resource))) 270 | (when (fboundp 'ghub--host) 271 | (unless (string-equal (url-host url) (ghub--host)) 272 | (error "Bad link"))) 273 | (ghubp-request method (url-filename url) params data))) 274 | 275 | (defun ghubp-follow-get (resource &optional params data) 276 | "GET wrapper for `ghubp-follow'. 277 | See that documentation for RESOURCE, PARAMS, and DATA." 278 | (ghubp--follow 'get resource params data)) 279 | (defun ghubp-follow-put (resource &optional params data) 280 | "PUT wrapper for `ghubp-follow'. 281 | See that documentation for RESOURCE, PARAMS, and DATA." 282 | (ghubp--follow 'put resource params data)) 283 | (defun ghubp-follow-head (resource &optional params data) 284 | "HEAD wrapper for `ghubp-follow'. 285 | See that documentation for RESOURCE, PARAMS, and DATA." 286 | (ghubp--follow 'head resource params data)) 287 | (defun ghubp-follow-post (resource &optional params data) 288 | "POST wrapper for `ghubp-follow'. 289 | See that documentation for RESOURCE, PARAMS, and DATA." 290 | (ghubp--follow 'post resource params data)) 291 | (defun ghubp-follow-patch (resource &optional params data) 292 | "PATCH wrapper for `ghubp-follow'. 293 | See that documentation for RESOURCE, PARAMS, and DATA." 294 | (ghubp--follow 'patch resource params data)) 295 | (defun ghubp-follow-delete (resource &optional params data) 296 | "DELETE wrapper for `ghubp-follow'. 297 | See that documentation for RESOURCE, PARAMS, and DATA." 298 | (ghubp--follow 'delete resource params data)) 299 | 300 | (defun ghubp-base-html-url () 301 | "Get the base HTML URL from `ghub-default-host'." 302 | (if-let ((host (car (ignore-errors 303 | (process-lines "git" "config" "github.host"))))) 304 | (and (string-match (rx bos (group (* any)) "/api/v3" eos) host) 305 | (match-string 1 host)) 306 | "https://github.com")) 307 | 308 | (defun ghubp-host () 309 | "Exposes `ghub--host'." 310 | (ghub--host)) 311 | 312 | (defun ghubp-username () 313 | "Exposes `ghub--username'." 314 | (ghub--username (ghub--host 'github))) 315 | 316 | (defun ghubp-token (package) 317 | "Exposes `ghub--token' for PACKAGE in a friendly way." 318 | (let* ((host (ghub--host)) 319 | (user (ghub--username host))) 320 | (ghub--token host user package t))) 321 | 322 | 323 | ;;; Errors: 324 | (define-error 'ghubp-error "Ghub+ error" 'ghub-error) 325 | (define-error 'ghubp-error-review-is-active "This review is active" 'ghubp-error) 326 | 327 | 328 | ;;; Issues: 329 | 330 | (defapiget-ghubp "/issues" 331 | "List all issues assigned to the authenticated user across all 332 | visible repositories including owned repositories, member 333 | repositories, and organization repositories." 334 | "issues/#list-issues") 335 | 336 | (defapiget-ghubp "/user/issues" 337 | "List all issues across owned and member repositories assigned 338 | to the authenticated user." 339 | "issues/#list-issues") 340 | 341 | (defapiget-ghubp "/orgs/:org/issues" 342 | "List all issues for a given organization assigned to the 343 | authenticated user." 344 | "issues/#list-issues" 345 | (org) "/org/:org.login/issues") 346 | 347 | (defapiget-ghubp "/repos/:owner/:repo/issues" 348 | "List issues for a repository." 349 | "issues/#list-issues-for-a-repository" 350 | (repo) "/repos/:repo.owner.login/:repo.name/issues") 351 | 352 | (defapiget-ghubp "/repos/:owner/:repo/issues/:number" 353 | "Get a single issue." 354 | "issues/#get-a-single-issue" 355 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number") 356 | 357 | (defapipost-ghubp "/repos/:owner/:repo/issues" 358 | "Create an issue. 359 | Any user with pull access to a repository can create an issue." 360 | "issues/#create-an-issue" 361 | (repo) "/repos/:repo.owner.login/:repo.name/issues") 362 | 363 | (defapipatch-ghubp "/repos/:owner/:repo/issues/:number" 364 | "Edit an issue. 365 | Issue owners and users with push access can edit an issue." 366 | "issues/#edit-an-issue" 367 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number") 368 | 369 | (defapiput-ghubp "/repos/:owner/:repo/issues/:number/lock" 370 | "Lock an issue. 371 | Users with push access can lock an issue's conversation." 372 | "issues/#lock-an-issue" 373 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number") 374 | 375 | (defapidelete-ghubp "/repos/:owner/:repo/issues/:number/lock" 376 | "Unlock an issue 377 | Users with push access can unlock an issue's conversation." 378 | "issues/#unlock-an-issue" 379 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number") 380 | 381 | 382 | ;;; Issue Assignees: 383 | 384 | (defapiget-ghubp "/repos/:owner/:repo/assignees" 385 | "List assignees. 386 | This call lists all the available assignees to which issues may 387 | be assigned." 388 | "issues/assignees/#list-assignees" 389 | (repo) "/repos/:repo.owner.login/:repo.name/assignees") 390 | 391 | (defapiget-ghubp "/repos/:owner/:repo/assignees/:assignee" 392 | ;; todo: sugar to handle valid 404 response 393 | "Check assignee. 394 | You may also check to see if a particular user is an assignee for 395 | a repository." 396 | "issues/assignees/#check-assignee" 397 | (repo user) "/repos/:repo.owner.login/:repo.name/assignees/:user.login") 398 | 399 | (defapipost-ghubp "/repos/:owner/:repo/issues/:number/assignees" 400 | "Add assignees to an Issue. 401 | This call adds the users passed in the assignees key (as their 402 | logins) to the issue." 403 | "issues/assignees/#add-assignees-to-an-issue" 404 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/assignees" 405 | :pre-process-data 406 | (lambda (users) 407 | `((assignees . ,(ghubp-get-in-all '(login) users))))) 408 | 409 | (defapidelete-ghubp "/repos/:owner/:repo/issues/:number/assignees" 410 | "Remove assignees from an Issue. 411 | This call removes the users passed in the assignees key (as their 412 | logins) from the issue." 413 | "issues/assignees/#remove-assignees-from-an-issue" 414 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/assignees" 415 | :pre-process-data 416 | (lambda (users) 417 | `((assignees . ,(ghubp-get-in-all '(login) users))))) 418 | 419 | 420 | ;;; Issue Comments: 421 | 422 | (defapiget-ghubp "/repos/:owner/:repo/issues/:number/comments" 423 | "List comments on an issue. 424 | Issue Comments are ordered by ascending ID." 425 | "issues/comments/#list-comments-on-an-issue" 426 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/comments") 427 | 428 | (defapiget-ghubp "/repos/:owner/:repo/issues/comments" 429 | "List comments in a repository. 430 | By default, Issue Comments are ordered by ascending ID." 431 | "issues/comments/#list-comments-in-a-repository" 432 | (repo) "/repos/:repo.owner.login/:repo.name/issues/comments") 433 | 434 | (defapiget-ghubp "/repos/:owner/:repo/issues/comments/:id" 435 | "Get a single comment." 436 | "issues/comments/#get-a-single-comment" 437 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/comments/:thread.id") 438 | 439 | (defapipatch-ghubp "/repos/:owner/:repo/issues/:number/comments" 440 | "Create a comment." 441 | "issues/comments/#create-a-comment" 442 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/comments") 443 | 444 | (defapipatch-ghubp "/repos/:owner/:repo/issues/comments/:id" 445 | "Edit a comment." 446 | "issues/comments/#edit-a-comment" 447 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/comments/:thread.id") 448 | 449 | (defapidelete-ghubp "/repos/:owner/:repo/issues/comments/:id" 450 | "Delete a comment." 451 | "issues/comments/#delete-a-comment" 452 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/comments/:thread.id") 453 | 454 | 455 | ;;; Issue Events: 456 | 457 | (defapiget-ghubp "/repos/:owner/:repo/issues/:number/events" 458 | ;; note: :number changed from :issue_number for consistency 459 | "List events for an issue." 460 | "issues/events/#list-events-for-an-issue" 461 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/events") 462 | 463 | (defapiget-ghubp "/repos/:owner/:repo/issues/events" 464 | "List events for a repository." 465 | "issues/events/#list-events-for-a-repository" 466 | (repo) "/repos/:repo.owner.login/:repo.name/issues/events") 467 | 468 | (defapiget-ghubp "/repos/:owner/:repo/issues/events/:id" 469 | "Get a single event." 470 | "issues/events/#get-a-single-event" 471 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/events/:thread.id") 472 | 473 | 474 | ;;; Issue Labels: 475 | 476 | (defapiget-ghubp "/repos/:owner/:repo/labels" 477 | "List all labels for this repository." 478 | "issues/labels/#list-all-labels-for-this-repository" 479 | (repo) "/repos/:repo.owner.login/:repo.name/labels") 480 | 481 | (defapiget-ghubp "/repos/:owner/:repo/labels/:name" 482 | "Get a single label." 483 | "issues/labels/#get-a-single-label" 484 | (repo label) "/repos/:repo.owner.login/:repo.name/labels/:label.name") 485 | 486 | (defapipost-ghubp "/repos/:owner/:repo/labels" 487 | "Create a label." 488 | "issues/labels/#create-a-label" 489 | (repo) "/repos/:repo.owner.login/:repo.name/labels") 490 | 491 | (defapipatch-ghubp "/repos/:owner/:repo/labels/:name" 492 | "Update a label." 493 | "issues/labels/#update-a-label" 494 | (repo label) "/repos/:repo.owner.login/:repo.name/labels/:label.name") 495 | 496 | (defapidelete-ghubp "/repos/:owner/:repo/labels/:name" 497 | "Delete a label." 498 | "issues/labels/#deleted-a-label" 499 | (repo label) "/repos/:repo.owner.login/:repo.name/labels/:label.name") 500 | 501 | (defapiget-ghubp "/repos/:owner/:repo/issues/:number/labels" 502 | "List labels on an issue." 503 | "issues/labels/#list-labels-on-an-issue" 504 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/labels") 505 | 506 | (defapipost-ghubp "/repos/:owner/:repo/issues/:number/labels" 507 | "Add labels to an issue." 508 | "issues/labels/#add-labels-to-an-issue" 509 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/labels" 510 | :pre-process-data (apply-partially #'ghubp-get-in-all '(name))) 511 | 512 | (defapidelete-ghubp "/repos/:owner/:repo/issues/:number/labels/:name" 513 | "Remove a label from an issue." 514 | "issues/labels/#remove-a-label-from-an-issue" 515 | (repo issue label) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/labels/:label.name") 516 | 517 | (defapipatch-ghubp "/repos/:owner/:repo/issues/:number/labels" 518 | "Replace all labels for an issue." 519 | "issues/labels/#replace-all-labels-for-an-issue" 520 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/labels" 521 | :pre-process-data (apply-partially #'ghubp-get-in-all '(name))) 522 | 523 | (defapidelete-ghubp "/repos/:owner/:repo/issues/:number/labels" 524 | "Remove all labels from an issue." 525 | "issues/labels/#remove-all-labels-from-an-issue" 526 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/labels") 527 | 528 | (defapiget-ghubp "/repos/:owner/:repo/milestones/:number/labels" 529 | "Get labels for every issue in a milestone." 530 | "issues/labels/#get-labels-for-every-issue-in-a-milestone" 531 | (repo milestone) "/repos/:repo.owner.login/:repo.name/milestones/:milestone.number/labels") 532 | 533 | 534 | ;;; Issue Milestones: 535 | 536 | (defapiget-ghubp "/repos/:owner/:repo/milestones" 537 | "List milestones for a repository." 538 | "issues/milestones/#list-milestones-for-a-repository" 539 | (repo) "/repos/:repo.owner.login/:repo.name/milestones") 540 | 541 | (defapiget-ghubp "/repos/:owner/:repo/milestones/:number" 542 | "Get a single milestone." 543 | "issues/milestones/#get-a-single-milestone" 544 | (repo milestone) "/repos/:repo.owner.login/:repo.name/milestones/:milestone.number") 545 | 546 | (defapipost-ghubp "/repos/:owner/:repo/milestones" 547 | "Create a milestone." 548 | "issues/milestones/#create-a-milestone" 549 | (repo) "/repos/:repo.owner.login/:repo.name/milestones") 550 | 551 | (defapipatch-ghubp "/repos/:owner/:repo/milestones/:number" 552 | "Update a milestone." 553 | "issues/milestones/#create-a-milestone" 554 | (repo milestone) "/repos/:repo.owner.login/:repo.name/milestones/:milestone.number") 555 | 556 | (defapidelete-ghubp "/repos/:owner/:repo/milestones/:number" 557 | "Delete a milestone." 558 | "issues/milestones/#delete-a-milestone" 559 | (repo milestone) "/repos/:repo.owner.login/:repo.name/milestones/:milestone.number") 560 | 561 | 562 | ;;; Organizations: 563 | 564 | (defapiget-ghubp "/user/orgs" 565 | "List organizations for the authenticated user." 566 | "orgs/#list-your-organizations") 567 | 568 | (defapiget-ghubp "/organizations" 569 | "Lists all organizations in the order that they were created on GitHub." 570 | "orgs/#list-all-organizations") 571 | 572 | (defapiget-ghubp "/users/:username/orgs" 573 | "List public organization memberships for the specified user." 574 | "orgs/#list-user-organizations" 575 | (user) "/users/:user.login/orgs") 576 | 577 | (defapiget-ghubp "/orgs/:org" 578 | "Get an organization." 579 | "orgs/#get-an-organization" 580 | (org) "/orgs/:org.login") 581 | 582 | (defapipatch-ghubp "/orgs/:org" 583 | "Edit an organization." 584 | "orgs/#edit-an-organization" 585 | (org) "/orgs/:org.login") 586 | 587 | 588 | ;;; Pull Request: 589 | 590 | (defapiget-ghubp "/repos/:owner/:repo/pulls" 591 | "List pull requests." 592 | "pulls/#list-pull-requests" 593 | (repo) "/repos/:repo.owner.login/:repo.name/pulls") 594 | 595 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number" 596 | "Get a single pull request." 597 | "pulls/#get-a-single-pull-request" 598 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number") 599 | 600 | (defapipost-ghubp "/repos/:owner/:repo/pulls" 601 | "Create a pull request." 602 | "pulls/#create-a-pull-request" 603 | (repo) "/repos/:repo.owner.login/:repo.name/pulls") 604 | 605 | (defapipatch-ghubp "/repos/:owner/:repo/pulls/:number" 606 | "Update a pull request." 607 | "pulls/#update-a-pull-request" 608 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number") 609 | 610 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/commits" 611 | "List commits on a pull request." 612 | "pulls/#list-commits-on-a-pull-request" 613 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/commits") 614 | 615 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/files" 616 | "List pull request files." 617 | "pulls/#list-pull-requests-files" 618 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/files") 619 | 620 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/merge" 621 | "Get if a pull request has been merged." 622 | "pulls/#get-if-a-pull-request-has-been-merged" 623 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/merge") 624 | 625 | (defapiput-ghubp "/repos/:owner/:repo/pulls/:number/merge" 626 | "Merge a pull request (Merge Button)" 627 | "pulls/#merge-a-pull-request-merge-button" 628 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/merge") 629 | 630 | 631 | ;;; Pull Request Reviews: 632 | 633 | (defapiget-ghubp "/repos/:owner/:repo/collaborators" 634 | "List collaborators. 635 | This call lists all the repo's collaborators." 636 | "repos/collaborators/#list-collaborators" 637 | (repo) "/repos/:repo.owner.login/:repo.name/collaborators") 638 | 639 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/reviews" 640 | "List reviews on a pull request." 641 | "pulls/reviews/#list-reviews-on-a-pull-request" 642 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews") 643 | 644 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/reviews/:id" 645 | "Get a single review." 646 | "pulls/reviews/#list-reviews-on-a-pull-request" 647 | (repo pull-request review) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews/:review.id") 648 | 649 | (defapidelete-ghubp "/repos/:owner/:repo/pulls/:number/reviews/:id" 650 | "Delete a pending review." 651 | "pulls/reviews/#delete-a-pending-review" 652 | (repo pull-request review) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews/:review.id" 653 | :condition-case 654 | (ghubp-catch* 655 | (422 (signal 'ghubp-error-review-is-active nil)))) 656 | 657 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/reviews/:id/comments" 658 | "Get comments for a single review." 659 | "pulls/reviews/#get-comments-for-a-single-review" 660 | (repo pull-request review) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews/:review.id/comments") 661 | 662 | (defapipost-ghubp "/repos/:owner/:repo/pulls/:number/reviews" 663 | "Create a pull request review." 664 | "pulls/reviews/#create-a-pull-request-review" 665 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews") 666 | 667 | (defapipost-ghubp "/repos/:owner/:repo/pulls/:number/reviews/:id/events" 668 | "Submit a pull request review." 669 | "pulls/reviews/#submit-a-pull-request-review" 670 | (repo pull-request review) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews/:review.id/events") 671 | 672 | (defapiput-ghubp "/repos/:owner/:repo/pulls/:number/reviews/:id/dismissals" 673 | "Dismiss a pull request review." 674 | "pulls/reviews/#dismiss-a-pull-request-review" 675 | (repo pull-request review) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/reviews/:review.id/dismissals") 676 | 677 | 678 | ;;; Pull Request Review Comments: 679 | 680 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/comments" 681 | "List comments on a pull request." 682 | "pulls/comments/#list-comments-on-a-pull-request" 683 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/comments") 684 | 685 | (defapiget-ghubp "/repos/:owner/:repo/pulls/comments" 686 | "List comments in a repository." 687 | "pulls/comments/#list-comments-in-a-repository" 688 | (repo) "/repos/:repo.owner.login/:repo.name/pulls/comments") 689 | 690 | (defapiget-ghubp "/repos/:owner/:repo/pulls/comments/:id" 691 | "Get a single comment." 692 | "pulls/comments/#get-a-single-comment" 693 | (repo thread) "/repos/:repo.owner.login/:repo.name/pulls/comments/:thread.id") 694 | 695 | (defapipost-ghubp "/repos/:owner/:repo/pulls/:number/comments" 696 | "Create a comment." 697 | "pulls/comments/#create-a-comment" 698 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/comments") 699 | 700 | (defapipatch-ghubp "/repos/:owner/:repo/pulls/comments/:id" 701 | "Edit a comment." 702 | "pulls/comments/#edit-a-comment" 703 | (repo thread) "/repos/:repo.owner.login/:repo.name/pulls/comments/:thread.id") 704 | 705 | (defapidelete-ghubp "/repos/:owner/:repo/pulls/comments/:id" 706 | "Delete a comment." 707 | "pulls/comments/#delete-a-comment" 708 | (repo thread) "/repos/:repo.owner.login/:repo.name/pulls/comments/:thread.id") 709 | 710 | 711 | ;;; Pull Request Review Requests: 712 | 713 | (defapiget-ghubp "/repos/:owner/:repo/pulls/:number/requested_reviewers" 714 | "List review requests." 715 | "pulls/review_requests/#list-review-requests" 716 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/requested_reviewers") 717 | 718 | (defapipost-ghubp "/repos/:owner/:repo/pulls/:number/requested_reviewers" 719 | "Create a review request." 720 | "pulls/review_requests/#create-a-review-request" 721 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/requested_reviewers" 722 | :pre-process-data 723 | (lambda (users) 724 | `((reviewers . ,(ghubp-get-in-all '(login) users))))) 725 | 726 | (defapidelete-ghubp "/repos/:owner/:repo/pulls/:number/requested_reviewers" 727 | "Delete a review request." 728 | "pulls/review_requests/#delete-a-review-request" 729 | (repo pull-request) "/repos/:repo.owner.login/:repo.name/pulls/:pull-request.number/requested_reviewers") 730 | 731 | 732 | ;;; Reactions: 733 | 734 | (defapiget-ghubp "/repos/:owner/:repo/comments/:id/reactions" 735 | "List reactions for a commit comment." 736 | "reactions/#list-reactions-for-a-commit-comment" 737 | (repo thread) "/repos/:repo.owner.login/:repo.name/comments/:thread.id/reactions") 738 | 739 | (defapipost-ghubp "/repos/:owner/:repo/comments/:id/reactions" 740 | "Create reaction for a commit comment." 741 | "reactions/#create-reaction-for-a-commit-comment" 742 | (repo thread) "/repos/:repo.owner.login/:repo.name/comments/:thread.id/reactions") 743 | 744 | (defapiget-ghubp "/repos/:owner/:repo/issues/:number/reactions" 745 | "List reactions for an issue." 746 | "reactions/#list-reactions-for-an-issue" 747 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/reactions") 748 | 749 | (defapipost-ghubp "/repos/:owner/:repo/issues/:number/reactions" 750 | "Create reaction for an issue." 751 | "reactions/#create-reaction-for-an-issue" 752 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/reactions") 753 | 754 | (defapiget-ghubp "/repos/:owner/:repo/issues/comments/:id/reactions" 755 | "List reactions for an issue comment." 756 | "reactions/#list-reactions-for-an-issue-comment" 757 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/comments/:thread.id/reactions") 758 | 759 | (defapipost-ghubp "/repos/:owner/:repo/issues/comments/:id/reactions" 760 | "Create reaction for an issue comment." 761 | "reactions/#create-reaction-for-an-issue-comment" 762 | (repo thread) "/repos/:repo.owner.login/:repo.name/issues/comments/:thread.id/reactions") 763 | 764 | (defapiget-ghubp "/repos/:owner/:repo/pulls/comments/:id/reactions" 765 | "List reactions for a pull request review comment." 766 | "reactions/#list-reactions-for-a-pull-request-review-comment" 767 | (repo thread) "/repos/:repo.owner.login/:repo.name/pulls/comments/:thread.id/reactions") 768 | 769 | (defapipost-ghubp "/repos/:owner/:repo/pulls/comments/:id/reactions" 770 | "Create reaction for a pull request review comment." 771 | "reactions/#create-reaction-for-a-pull-request-review-comment" 772 | (repo thread) "/repos/:repo.owner.login/:repo.name/pulls/comments/:thread.id/reactions") 773 | 774 | (defapidelete-ghubp "/reactions/:id" 775 | "Delete a reaction." 776 | "reactions/#delete-a-reaction" 777 | (thread) "/reactions/:thread.id") 778 | 779 | 780 | ;;; Repositories: 781 | 782 | (defapiget-ghubp "/user/repos" 783 | "List your repositories. 784 | List repositories that are accessible to the authenticated user. 785 | 786 | This includes repositories owned by the authenticated user, 787 | repositories where the authenticated user is a collaborator, and 788 | repositories that the authenticated user has access to through an 789 | organization membership." 790 | "repos/#list-your-repositories") 791 | 792 | (defapiget-ghubp "/users/:username/repos" 793 | "List user repositories. 794 | List public repositories for the specified user." 795 | "repos/#list-user-repositories" 796 | (user) "/users/:user.login/repos") 797 | 798 | (defapiget-ghubp "/orgs/:org/repos" 799 | "List organization repositories. 800 | List repositories for the specified org." 801 | "repos/#list-organization-repositories" 802 | (org) "/orgs/:org.login/repos") 803 | 804 | (defapiget-ghubp "/repositories" 805 | "List all public repositories. 806 | This provides a dump of every public repository, in the order 807 | that they were created." 808 | "repos/#list-all-public-repositories") 809 | 810 | (defapipost-ghubp "/user/repos" 811 | "Create. 812 | Create a new repository for the authenticated user. (Currently 813 | not enabled for Integrations)." 814 | "repos/#create") 815 | 816 | (defapipost-ghubp "/orgs/:org/repos" 817 | "Create a new repository in this organization. 818 | The authenticated user must be a member of the specified 819 | organization." 820 | "repos/#create" 821 | (org) "/orgs/:org.login/repos") 822 | 823 | (defapiget-ghubp "/repos/:owner/:repo" 824 | "Get a specific repository object." 825 | "repos/#get" 826 | (repo) "/repos/:repo.owner.login/:repo.name" 827 | :condition-case 828 | (ghubp-catch* 829 | (404 nil))) 830 | 831 | 832 | ;;; Branches: 833 | 834 | (defapiget-ghubp "/repos/:owner/:repo/branches/:branch" 835 | "Get branch" 836 | "repos/branches/#get-branch" 837 | (repo branch) "/repos/:repo.owner.login/:repo.name/branches/:branch.name" 838 | :condition-case 839 | (ghubp-catch* 840 | (404 nil))) 841 | 842 | 843 | ;;; Users: 844 | (defapiget-ghubp "/users/:username" 845 | "Get a single user." 846 | "users/#get-a-single-user" 847 | (user) "/users/:user.login" 848 | :condition-case 849 | (ghubp-catch* 850 | (404 nil))) 851 | 852 | (defapiget-ghubp "/user" 853 | "Get the authenticated user." 854 | "users/#get-the-authenticated-user") 855 | 856 | (defapipatch-ghubp "/user" 857 | "Update the authenticated user." 858 | "users/#update-the-authenticated-user") 859 | 860 | (defapiget-ghubp "/users" 861 | "Get all users. 862 | Lists all users, in the order that they signed up on GitHub. This 863 | list includes personal user accounts and organization accounts." 864 | "users/#get-all-users") 865 | 866 | ;; Users - Emails 867 | 868 | (defapiget-ghubp "/user/emails" 869 | "List email addresses for a user." 870 | "users/emails/#list-email-addresses-for-a-user") 871 | 872 | (defapiget-ghubp "/user/public_emails" 873 | "List public email addresses for a user." 874 | "users/emails/#list-public-email-addresses-for-a-user") 875 | 876 | (defapipost-ghubp "/user/emails" 877 | "Add email address(es). 878 | You can post a single email address or an array of addresses." 879 | "users/emails/#add-email-addresses") 880 | 881 | (defapidelete-ghubp "/user/emails" 882 | "Delete email address(es). 883 | You can post a single email address or an array of addresses." 884 | "users/emails/#add-email-addresses") 885 | 886 | (defapipatch-ghubp "/user/email/visibility" 887 | "Toggle primary email visibility." 888 | "users/emails/#toggle-primary-email-visibility") 889 | 890 | ;; Users - Followers 891 | 892 | (defapiget-ghubp "/users/:username/followers" 893 | "List a user's followers." 894 | "users/followers/#list-followers-of-a-user" 895 | (user) "/users/:user.login/followers") 896 | 897 | (defapiget-ghubp "/user/followers" 898 | "List the authenticated user's followers." 899 | "users/followers/#list-followers-of-a-user") 900 | 901 | (defapiget-ghubp "/users/:username/following" 902 | "List who USER is following." 903 | "users/followers/#list-users-followed-by-another-user" 904 | (user) "/users/:user.login/following") 905 | 906 | (defapiget-ghubp "/user/following" 907 | "List who the authenticated user is following." 908 | "users/followers/#list-users-followed-by-another-user") 909 | 910 | (defapiget-ghubp "/user/following/:username" 911 | "Check if you are following USER." 912 | "users/followers/#check-if-you-are-following-a-user" 913 | (user) "/user/following/:user.login") 914 | 915 | (defapiget-ghubp "/users/:username/following/:target_user" 916 | "Check if USER-1 follows USER-2." 917 | "users/followers/#check-if-you-are-following-a-user" 918 | (user-1 user-2) "/users/:user-1.login/following/:user-2.login") 919 | 920 | (defapiput-ghubp "/user/following/:username" 921 | "Follow USER." 922 | "users/followers/#follow-a-user" 923 | (user) "/user/following/:user.login") 924 | 925 | (defapidelete-ghubp "/user/following/:username" 926 | "Unfollow USER." 927 | "users/followers/#unfollow-a-user" 928 | (user) "/user/following/:user.login") 929 | 930 | ;; Users - Git SSH Keys 931 | 932 | (defapiget-ghubp "/users/:username/keys" 933 | "Lists the verified public keys for a user. 934 | This is accessible by anyone." 935 | "users/keys/#list-public-keys-for-a-user" 936 | (user) "/users/:user.login/keys") 937 | 938 | (defapiget-ghubp "/user/keys" 939 | "List your public keys." 940 | "users/keys/#list-your-public-keys") 941 | 942 | (defapiget-ghubp "/user/keys/:id" 943 | "Get a single public key." 944 | "users/keys/#get-a-single-public-key" 945 | (key) "/user/keys/:key.id") 946 | 947 | (defapiput-ghubp "/user/keys" 948 | "Create a public key." 949 | "users/keys/#create-a-public-key") 950 | 951 | (defapidelete-ghubp "/user/keys/:id" 952 | "Delete a single public key." 953 | "users/keys/#get-a-single-public-key" 954 | (key) "/user/keys/:key.id") 955 | 956 | ;; Users - GPG Keys 957 | 958 | ;; TODO: Currently in preview. 959 | 960 | ;; https://developer.github.com/v3/users/gpg_keys/ 961 | 962 | ;; Users - Blocking 963 | 964 | ;; TODO: Currently in preview. 965 | 966 | ;; https://developer.github.com/v3/users/blocking/ 967 | 968 | ;; Activity - Notifications 969 | 970 | ;; TODO: It would be nice if ghub+ could offer 'smart' polling for 971 | ;; notifications to trim down on API requests. This smart polling is 972 | ;; supported by GitHub for notifications in particular. If done 973 | ;; right, we could offer a 'push'-type interface to handle when new 974 | ;; notifications are received. 975 | 976 | ;; (ghubp-notifications-{start,stop}-polling) 977 | ;; ghubp-notifications-received-hook 978 | 979 | (defapiget-ghubp "/notifications" 980 | "Get the user's notifications." 981 | "activity/notifications/#list-your-notifications") 982 | 983 | (defapiget-ghubp "/repos/:owner/:repo/notifications" 984 | "List your notifications in a repository." 985 | "activity/notifications/#list-your-notifications-in-a-repository" 986 | (repo) "/repos/:repo.owner.login/:repo.name/notifications") 987 | 988 | (defapiput-ghubp "/notifications" 989 | "Mark as read. 990 | Marking a notification as \"read\" removes it from the default 991 | view on GitHub." 992 | "activity/notifications/#mark-as-read") 993 | 994 | (defapiput-ghubp "/repos/:owner/:repo/notifications" 995 | "Mark notifications as read in a repository. 996 | Marking all notifications in a repository as \"read\" removes 997 | them from the default view on GitHub." 998 | "activity/notifications/#mark-notifications-as-read-in-a-repository" 999 | (repo) "/repos/:repo.owner.login/:repo.name/notifications") 1000 | 1001 | (defapiget-ghubp "/notifications/threads/:id" 1002 | "View a single thread." 1003 | "activity/notifications/#view-a-single-thread" 1004 | (thread) "/notifications/threads/:thread.id") 1005 | 1006 | (defapipatch-ghubp "/notifications/threads/:id" 1007 | "Mark a thread as read." 1008 | "activity/notifications/#mark-a-thread-as-read" 1009 | (thread) "/notifications/threads/:thread.id") 1010 | 1011 | (defapiget-ghubp "/notifications/threads/:id/subscription" 1012 | "Get a thread subscription. 1013 | This checks to see if the current user is subscribed to a 1014 | thread." 1015 | "activity/notifications/#get-a-thread-subscription" 1016 | (thread) "/notifications/threads/:thread.id/subscription") 1017 | 1018 | (defapiput-ghubp "/notifications/threads/:id/subscription" 1019 | "Set a thread subscription. 1020 | This lets you subscribe or unsubscribe from a conversation. 1021 | Unsubscribing from a conversation mutes all future 1022 | notifications (until you comment or get @mentioned once more)." 1023 | "activity/notifications/#set-a-thread-subscription" 1024 | (thread) "/notifications/threads/:thread.id/subscription") 1025 | 1026 | (defapidelete-ghubp "/notifications/threads/:id/subscription" 1027 | "Delete a thread subscription." 1028 | "activity/notifications/#delete-a-thread-subscription" 1029 | (thread) "/notifications/threads/:thread.id/subscription") 1030 | 1031 | 1032 | ;;; Unfiled: 1033 | 1034 | (defapiget-ghubp "/repos/:owner/:repo/commits/:ref/statuses" 1035 | "List statuses for a specific ref" 1036 | "repos/statuses/#list-statuses-for-a-specific-ref" 1037 | (repo ref) "/repos/:repo.owner.login/:repo.name/commits/:ref/statuses") 1038 | 1039 | (defapiget-ghubp "/repos/:owner/:repo/commits/:ref/status" 1040 | "Get the combined status for a specific ref" 1041 | "repos/statuses/#get-the-combined-status-for-a-specific-ref" 1042 | (repo ref) "/repos/:repo.owner.login/:repo.name/commits/:ref/status" 1043 | :condition-case 1044 | (ghubp-catch* 1045 | (404 nil))) 1046 | 1047 | (defapipost-ghubp "/repos/:owner/:repo/forks" 1048 | "Create a fork for the authenticated user." 1049 | "repos/forks/#create-a-fork" 1050 | (repo) "/repos/:repo.owner.login/:repo.name/forks") 1051 | 1052 | (defapipost-ghubp "/repos/:owner/:repo/issues/:number/comments" 1053 | "Post a comment to an issue" 1054 | "issues/comments/#create-a-comment" 1055 | (repo issue) "/repos/:repo.owner.login/:repo.name/issues/:issue.number/comments") 1056 | 1057 | (defapiget-ghubp "/repos/:owner/:repo/commits" 1058 | "List commits on a repository" 1059 | "repos/commits/#list-commits-on-a-repository" 1060 | (repo) "/repos/:repo.owner.login/:repo.name/commits") 1061 | 1062 | (defun ghubp-url-parse (url) 1063 | "Parse URL for its type and API callback. 1064 | 1065 | A cons cell is returned. The car is one of 1066 | 1067 | - `issue' 1068 | - `pull-request' 1069 | 1070 | and the cdr is a callback suitable for `ghub-get', etc." 1071 | (let ((callback (url-filename (url-generic-parse-url url)))) 1072 | (cons 1073 | (cond 1074 | ((string-match-p (rx bol "/repos/" (+? any) "/" (+? any) "/issues/" (+ digit) eol) 1075 | callback) 1076 | 'issue) 1077 | ((string-match-p (rx bol "/repos/" (+? any) "/" (+? any) "/pulls/" (+ digit) eol) 1078 | callback) 1079 | 'pull-request) 1080 | (t 'unknown)) 1081 | callback))) 1082 | 1083 | (provide 'ghub+) 1084 | ;;; ghub+.el ends here 1085 | -------------------------------------------------------------------------------- /test/ert-tests.el: -------------------------------------------------------------------------------- 1 | (require 'ghub+) 2 | 3 | (load "linter") 4 | 5 | (ert-deftest basic () 6 | (should 7 | (let* ((repo '((owner (login . "vermiculus")) 8 | (name . "ghub-plus"))) 9 | (repo (ghubp-get-repos-owner-repo repo))) 10 | (= 82884749 (alist-get 'id repo))))) 11 | 12 | (ert-deftest ghubp-test-ratelimit-utils () 13 | (let ((ghub-response-headers 14 | '(("X-RateLimit-Limit" . "5000") 15 | ("X-RateLimit-Remaining" . "4774") 16 | ("X-RateLimit-Reset" . "1501809984")))) 17 | (let-alist (ghubp-ratelimit) 18 | (should (equal .remaining 4774)) 19 | (should (equal .reset '(22915 52544)))))) 20 | 21 | (ert-deftest lint-unused-args () 22 | (should (lint "ghub+.el" #'lint-unused-args 'per-form))) 23 | 24 | (ert-deftest lint-undeclared-args () 25 | (should (lint "ghub+.el" #'lint-undeclared-standard-args))) 26 | 27 | (ert-deftest lint-ext-reference-in-name () 28 | (should (lint "ghub+.el" #'lint-ext-reference-in-name 'per-form))) 29 | 30 | (ert-deftest linter-selftest () 31 | (message ">>> Start linter self-tests") 32 | 33 | (should (lint-unused-args '(defapiget-ghubp "/rate_limit" "" "" (repo issue) "/:repo.thing"))) 34 | (should (lint-unused-args '(defapiget-ghubp "/rate_limit" "" "" (repo) "/:repo.thing/:issue.thing"))) 35 | (should (lint-ext-reference-in-name '(defapiget-ghubp "/rate.limit" "" "" (repo) "/:repo.thing/:issue.thing"))) 36 | (should-not (lint-unused-args '(defapiget-ghubp "/some_call_with_no_args" "some-desc" "some-url" 37 | :post-process (lambda (o) (ghubp--post-process o '(subject)))))) 38 | (should-not (lint-unused-args '(defapiget-ghubp "/some_call_with_no_args" "some-desc" "some-url"))) 39 | 40 | (should (lint-standard-args-undeclared--internal 41 | '(defapiget-ghubp "/some_call_with_new_args" "some-doc" "some-link" (repo issue weird) "") 42 | '(repo issue))) 43 | (should-not (lint-standard-args-undeclared--internal 44 | '(defapiget-ghubp "/some_call_with_new_args" "some-doc" "some-link" (repo issue) "") 45 | '(repo issue))) 46 | (should-not (lint-standard-args-undeclared--internal 47 | '(defapiget-ghubp "/some_call_with_new_args" "some-doc" "some-link" (repo) "") 48 | '(repo issue))) 49 | (should-not (lint-standard-args-undeclared--internal 50 | '(defapiget-ghubp "/some_call_with_new_args" "some-doc" "some-link" "") 51 | '(repo issue))) 52 | (should-not (lint-standard-args-undeclared--internal 53 | '(defapiget-ghubp "/some_call_with_new_args" "some-doc" "some-link") 54 | '(repo issue))) 55 | 56 | (message "<<< End linter self-tests")) 57 | -------------------------------------------------------------------------------- /test/linter.el: -------------------------------------------------------------------------------- 1 | (unless (require 'ghub+ nil 'noerror) 2 | (message "attempting to load ghub+ manually...") 3 | (load-file "./ghub+.el") 4 | (message "attempting to load ghub+ manually...done") 5 | (require 'ghub+)) 6 | 7 | (require 'subr-x) 8 | (require 'dash) 9 | (require 's) 10 | 11 | (cl-defun test-request-override (method resource &optional params &key query payload 12 | headers unpaginate noerror reader username auth host) 13 | (pcase (list method resource params query payload headers 14 | unpaginate noerror reader username auth host) 15 | (`("GET" "/repos/vermiculus/ghub-plus" nil nil nil nil nil nil nil nil nil nil) 16 | '((id . 82884749))))) 17 | (setq ghubp-request-override-function #'test-request-override) 18 | 19 | (defun lint-is-api-form-p (form) 20 | "Is FORM a defapi* macro call?" 21 | (and (s-prefix-p "defapi" (symbol-name (car form))) 22 | form)) 23 | 24 | (defun lint-get-forms (filename) 25 | "Read FILENAME and return a list of its Lisp forms." 26 | (let ((pos 0) forms) 27 | (with-temp-buffer 28 | (insert-file-contents filename) 29 | (condition-case _ 30 | (while t 31 | (when-let ((cell (read-from-string (buffer-string) pos))) 32 | (push (car cell) forms) 33 | (goto-char (setq pos (cdr cell))))) 34 | (error forms))) 35 | forms)) 36 | 37 | (defun lint-api-forms (filename) 38 | "From FILENAME, return a list of API forms." 39 | (-filter #'lint-is-api-form-p (lint-get-forms filename))) 40 | 41 | (defun lint-arg-appears-in-target-p (arg target-string) 42 | "Does symbol ARG appear in TARGET-STRING? 43 | Such that `apiwrap-resolve-api-params' would see it?" 44 | (and (stringp target-string) 45 | (or (s-contains-p (format ":%S." arg) target-string) 46 | (s-contains-p (format ":%S/" arg) target-string) 47 | (s-suffix-p (format ":%S" arg) target-string)))) 48 | 49 | (defun lint-target-to-args (target-string) 50 | (let (args) 51 | (with-temp-buffer 52 | (save-excursion 53 | (insert target-string)) 54 | (while (search-forward ":" nil t) 55 | (let ((arg (buffer-substring-no-properties 56 | (point) 57 | (1- (search-forward-regexp (rx (or "." "/" eol))))))) 58 | (unless (member arg args) 59 | (push arg args))))) 60 | args)) 61 | 62 | (defun lint-form-used-args (form) 63 | (when-let ((target (nth 5 form))) 64 | (when (stringp target) 65 | (lint-target-to-args target)))) 66 | 67 | (defun lint-form-declared-args (form) 68 | (when-let ((arg-list (nth 4 form))) 69 | (when (and (listp arg-list) (-all-p #'symbolp arg-list)) 70 | (mapcar #'symbol-name arg-list)))) 71 | 72 | (defun lint--sets-equal (l1 l2) 73 | (and (--all-p (member it l1) l2) 74 | (--all-p (member it l2) l1))) 75 | 76 | (defun lint-macro-to-method (msym) 77 | "Get the HTTP method corresponding to MSYM." 78 | (let ((s (symbol-name msym))) 79 | (upcase (substring s 6 (s-index-of "-" s))))) 80 | 81 | (defun lint-unused-args (form) 82 | "Check for any unused arguments in FORM. 83 | If there are unused arguments, print them out with `message' and 84 | return them. Return nil if there are no offenders." 85 | (let ((used (lint-form-used-args form)) 86 | (declared (lint-form-declared-args form)) 87 | (defapi (lint-format-defapi-form form)) 88 | unused undeclared) 89 | (unless (lint--sets-equal used declared) 90 | (setq unused (cl-set-difference declared used :test #'string=) 91 | undeclared (cl-set-difference used declared :test #'string=)) 92 | (dolist (arg unused) 93 | (message "Unused argument in '%s': %s" defapi arg)) 94 | (dolist (arg undeclared) 95 | (message "Undeclared argument in '%s': %s" defapi arg)) 96 | t))) 97 | 98 | (defun lint-format-defapi-form (form) 99 | "=> GET /some/thing/here" 100 | (concat (lint-macro-to-method (car form)) " " (cadr form))) 101 | 102 | (defun lint-standard-args-undeclared--get-backend (filename) 103 | "Get the backend declaration form from FILENAME." 104 | (with-temp-buffer 105 | (insert-file filename) 106 | (let ((needle "(apiwrap-new-backend")) 107 | (search-forward needle) 108 | (backward-char (length needle)) 109 | (read (current-buffer))))) 110 | 111 | (defun lint-standard-args-undeclared--get-std-vars (backend) 112 | "Get the list of standard vars from BACKEND." 113 | (mapcar #'car (cadr (nth 3 backend)))) 114 | 115 | (defun lint-standard-args-undeclared--internal (form std-args) 116 | "Non-nil if FORM uses args not in STD-ARGS" 117 | (let (fail) 118 | (when (listp (nth 4 form)) 119 | (dolist (std-arg (nth 4 form)) 120 | (unless (memq std-arg std-args) 121 | (setq fail t) 122 | (message "Undefined standard argument in '%s': %S" 123 | (lint-format-defapi-form form) 124 | std-arg)))) 125 | fail)) 126 | 127 | (defun lint-undeclared-standard-args (filename) 128 | "Wrapper for `lint-standard-args-undeclared--internal'." 129 | (let ((forms (lint-api-forms filename)) 130 | (stdargs (lint-standard-args-undeclared--get-std-vars 131 | (lint-standard-args-undeclared--get-backend filename))) 132 | fail) 133 | (dolist (form forms) 134 | (setq fail (or (lint-standard-args-undeclared--internal form stdargs) fail))) 135 | fail)) 136 | 137 | (defun lint-ext-reference-in-name (form) 138 | (s-contains-p "." (cadr form))) 139 | 140 | (defun lint (filename lint-function &optional per-form) 141 | "Run all linting checks on forms in FILENAME." 142 | (let (fail) 143 | (if per-form 144 | (dolist (form (lint-api-forms filename)) 145 | (setq fail (or (funcall lint-function form) fail))) 146 | (setq fail (funcall lint-function filename))) 147 | (not fail))) 148 | --------------------------------------------------------------------------------