Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

529 lines
21 KiB

  1. ;;; ghub-graphql.el --- access Github API using GrapthQL -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2016-2021 Jonas Bernoulli
  3. ;; Author: Jonas Bernoulli <jonas@bernoul.li>
  4. ;; Homepage: https://github.com/magit/ghub
  5. ;; Keywords: tools
  6. ;; SPDX-License-Identifier: GPL-3.0-or-later
  7. ;; This file is not part of GNU Emacs.
  8. ;; This file is free software; you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation; either version 3, or (at your option)
  11. ;; any later version.
  12. ;; This file is distributed in the hope that it will be useful,
  13. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;; GNU General Public License for more details.
  16. ;; For a copy of the GPL see https://www.gnu.org/licenses/gpl.txt.
  17. ;;; Code:
  18. (require 'ghub)
  19. (require 'gsexp)
  20. (require 'treepy)
  21. (eval-when-compile
  22. (require 'subr-x))
  23. ;;; Api
  24. (cl-defun ghub-graphql (graphql &optional variables
  25. &key username auth host
  26. headers silent
  27. callback errorback value extra)
  28. "Make a GraphQL request using GRAPHQL and VARIABLES.
  29. Return the response as a JSON-like alist. Even if the response
  30. contains `errors', do not raise an error. GRAPHQL is a GraphQL
  31. string. VARIABLES is a JSON-like alist. The other arguments
  32. behave as for `ghub-request' (which see)."
  33. (cl-assert (stringp graphql))
  34. (cl-assert (not (stringp variables)))
  35. (ghub-request "POST" "/graphql" nil :payload
  36. (json-encode `(("query" . ,graphql)
  37. ,@(and variables `(("variables" ,@variables)))))
  38. :headers headers :silent silent
  39. :username username :auth auth :host host
  40. :callback callback :errorback errorback
  41. :extra extra :value value))
  42. (cl-defun ghub-graphql-rate-limit (&key username auth host)
  43. "Return rate limit information."
  44. (let-alist (ghub-graphql
  45. "query { rateLimit { limit cost remaining resetAt }}"
  46. nil :username username :auth auth :host host)
  47. .data.rateLimit))
  48. (cl-defun ghub--repository-id (owner name &key username auth host)
  49. "Return the id of the repository specified by OWNER, NAME and HOST."
  50. (let-alist (ghub-graphql
  51. "query ($owner:String!, $name:String!) {
  52. repository(owner:$owner, name:$name) { id }
  53. }"
  54. `((owner . ,owner)
  55. (name . ,name))
  56. :username username :auth auth :host host)
  57. .data.repository.id))
  58. ;;; Api (drafts)
  59. (defconst ghub-fetch-repository
  60. '(query
  61. (repository
  62. [(owner $owner String!)
  63. (name $name String!)]
  64. name
  65. id
  66. createdAt
  67. updatedAt
  68. nameWithOwner
  69. (parent nameWithOwner)
  70. description
  71. homepageUrl
  72. (defaultBranchRef name)
  73. isArchived
  74. isFork
  75. isLocked
  76. isMirror
  77. isPrivate
  78. hasIssuesEnabled
  79. hasWikiEnabled
  80. (licenseInfo name)
  81. (stargazers totalCount)
  82. (watchers totalCount)
  83. (assignableUsers [(:edges t)]
  84. id
  85. login
  86. name)
  87. (issues [(:edges t)
  88. (:singular issue number)
  89. (orderBy ((field UPDATED_AT) (direction DESC)))]
  90. number
  91. state
  92. (author login)
  93. title
  94. createdAt
  95. updatedAt
  96. closedAt
  97. locked
  98. (milestone id)
  99. body
  100. (assignees [(:edges t)]
  101. id)
  102. (comments [(:edges t)]
  103. databaseId
  104. (author login)
  105. createdAt
  106. updatedAt
  107. body)
  108. (labels [(:edges t)]
  109. id))
  110. (labels [(:edges t)
  111. (:singular label id)]
  112. id
  113. name
  114. color
  115. description)
  116. (milestones [(:edges t)
  117. (:singular milestone id)]
  118. id
  119. number
  120. title
  121. createdAt
  122. updatedAt
  123. dueOn
  124. closedAt
  125. description)
  126. (pullRequests [(:edges t)
  127. (:singular pullRequest number)
  128. (orderBy ((field UPDATED_AT) (direction DESC)))]
  129. number
  130. state
  131. (author login)
  132. title
  133. createdAt
  134. updatedAt
  135. closedAt
  136. mergedAt
  137. locked
  138. maintainerCanModify
  139. isCrossRepository
  140. (milestone id)
  141. body
  142. (baseRef name
  143. (repository nameWithOwner))
  144. baseRefOid
  145. (headRef name
  146. (repository (owner login)
  147. nameWithOwner))
  148. headRefOid
  149. (assignees [(:edges t)]
  150. id)
  151. (reviewRequests [(:edges t)]
  152. (requestedReviewer "... on User { id }\n"))
  153. (comments [(:edges t)]
  154. databaseId
  155. (author login)
  156. createdAt
  157. updatedAt
  158. body)
  159. (labels [(:edges t)]
  160. id)))))
  161. (defconst ghub-fetch-repository-review-threads
  162. '(query
  163. (repository
  164. [(owner $owner String!)
  165. (name $name String!)]
  166. (pullRequests [(:edges t)
  167. (:singular pullRequest number)
  168. (orderBy ((field UPDATED_AT) (direction DESC)))]
  169. number
  170. baseRefOid
  171. headRefOid
  172. (reviewThreads [(:edges t)]
  173. id
  174. line
  175. originalLine
  176. diffSide
  177. (resolvedBy login)
  178. (comments [(:edges t)]
  179. id
  180. databaseId
  181. (author login)
  182. createdAt
  183. updatedAt
  184. body
  185. (replyTo databaseId)
  186. (originalCommit oid)
  187. path))))))
  188. (cl-defun ghub-fetch-repository (owner name callback
  189. &optional until
  190. &key username auth host forge
  191. headers errorback)
  192. "Asynchronously fetch forge data about the specified repository.
  193. Once all data has been collected, CALLBACK is called with the
  194. data as the only argument."
  195. (ghub--graphql-vacuum ghub-fetch-repository
  196. `((owner . ,owner)
  197. (name . ,name))
  198. callback until
  199. :narrow '(repository)
  200. :username username
  201. :auth auth
  202. :host host
  203. :forge forge
  204. :headers headers
  205. :errorback errorback))
  206. (cl-defun ghub-fetch-issue (owner name number callback
  207. &optional until
  208. &key username auth host forge
  209. headers errorback)
  210. "Asynchronously fetch forge data about the specified issue.
  211. Once all data has been collected, CALLBACK is called with the
  212. data as the only argument."
  213. (ghub--graphql-vacuum (ghub--graphql-prepare-query
  214. ghub-fetch-repository
  215. `(repository issues (issue . ,number)))
  216. `((owner . ,owner)
  217. (name . ,name))
  218. callback until
  219. :narrow '(repository issue)
  220. :username username
  221. :auth auth
  222. :host host
  223. :forge forge
  224. :headers headers
  225. :errorback errorback))
  226. (cl-defun ghub-fetch-pullreq (owner name number callback
  227. &optional until
  228. &key username auth host forge
  229. headers errorback)
  230. "Asynchronously fetch forge data about the specified pull-request.
  231. Once all data has been collected, CALLBACK is called with the
  232. data as the only argument."
  233. (ghub--graphql-vacuum (ghub--graphql-prepare-query
  234. ghub-fetch-repository
  235. `(repository pullRequests (pullRequest . ,number)))
  236. `((owner . ,owner)
  237. (name . ,name))
  238. callback until
  239. :narrow '(repository pullRequest)
  240. :username username
  241. :auth auth
  242. :host host
  243. :forge forge
  244. :headers headers
  245. :errorback errorback))
  246. (cl-defun ghub-fetch-review-threads (owner name number callback
  247. &optional until
  248. &key username auth host forge
  249. headers errorback)
  250. "Asynchronously fetch forge data about the review threads from a pull-request.
  251. Once all data has been collected, CALLBACK is called with the
  252. data as the only argument."
  253. (ghub--graphql-vacuum (ghub--graphql-prepare-query
  254. ghub-fetch-repository-review-threads
  255. `(repository pullRequests (pullRequest . ,number)))
  256. `((owner . ,owner)
  257. (name . ,name))
  258. callback until
  259. :narrow '(repository pullRequest)
  260. :username username
  261. :auth auth
  262. :host host
  263. :forge forge
  264. :headers headers
  265. :errorback errorback))
  266. ;;; Internal
  267. (cl-defstruct (ghub--graphql-req
  268. (:include ghub--req)
  269. (:constructor ghub--make-graphql-req)
  270. (:copier nil))
  271. (query nil :read-only t)
  272. (query-str nil :read-only nil)
  273. (variables nil :read-only t)
  274. (until nil :read-only t)
  275. (buffer nil :read-only t)
  276. (pages 0 :read-only nil))
  277. (cl-defun ghub--graphql-vacuum (query variables callback
  278. &optional until
  279. &key narrow username auth host forge
  280. headers errorback)
  281. "Make a GraphQL request using QUERY and VARIABLES.
  282. See Info node `(ghub)GraphQL Support'."
  283. (unless host
  284. (setq host (ghub--host forge)))
  285. (unless (or username (stringp auth) (eq auth 'none))
  286. (setq username (ghub--username host forge)))
  287. (ghub--graphql-retrieve
  288. (ghub--make-graphql-req
  289. :url (url-generic-parse-url
  290. (format "https://%s/graphql"
  291. (if (string-suffix-p "/v3" host)
  292. (substring host 0 -3)
  293. host)))
  294. :method "POST"
  295. :headers (ghub--headers headers host auth username forge)
  296. :handler 'ghub--graphql-handle-response
  297. :query query
  298. :variables variables
  299. :until until
  300. :buffer (current-buffer)
  301. :callback (let ((buf (current-buffer)))
  302. (if narrow
  303. (lambda (data)
  304. (let ((path narrow) key)
  305. (while (setq key (pop path))
  306. (setq data (cdr (assq key data)))))
  307. (ghub--graphql-set-mode-line buf nil)
  308. (funcall callback data))
  309. (lambda (data)
  310. (ghub--graphql-set-mode-line buf nil)
  311. (funcall callback data))))
  312. :errorback errorback)))
  313. (cl-defun ghub--graphql-retrieve (req &optional lineage cursor)
  314. (let ((p (cl-incf (ghub--graphql-req-pages req))))
  315. (when (> p 1)
  316. (ghub--graphql-set-mode-line req "Fetching page %s" p)))
  317. (setf (ghub--graphql-req-query-str req)
  318. (gsexp-encode
  319. (ghub--graphql-prepare-query
  320. (ghub--graphql-req-query req)
  321. lineage cursor)))
  322. (ghub--retrieve
  323. (let ((json-false nil))
  324. (ghub--encode-payload
  325. `((query . ,(ghub--graphql-req-query-str req))
  326. (variables . ,(ghub--graphql-req-variables req)))))
  327. req))
  328. (defun ghub--graphql-prepare-query (query &optional lineage cursor)
  329. (when lineage
  330. (setq query (ghub--graphql-narrow-query query lineage cursor)))
  331. (let ((loc (ghub--alist-zip query))
  332. variables)
  333. (cl-block nil
  334. (while t
  335. (let ((node (treepy-node loc)))
  336. (when (vectorp node)
  337. (let ((alist (cl-coerce node 'list))
  338. vars)
  339. (when (cadr (assq :edges alist))
  340. (push (list 'first 100) vars)
  341. (setq loc (treepy-up loc))
  342. (setq node (treepy-node loc))
  343. (setq loc (treepy-replace
  344. loc `(,(car node)
  345. ,(cadr node)
  346. (pageInfo endCursor hasNextPage)
  347. (edges (node ,@(cddr node))))))
  348. (setq loc (treepy-down loc))
  349. (setq loc (treepy-next loc)))
  350. (dolist (elt alist)
  351. (cond ((keywordp (car elt)))
  352. ((= (length elt) 3)
  353. (push (list (nth 0 elt)
  354. (nth 1 elt)) vars)
  355. (push (list (nth 1 elt)
  356. (nth 2 elt)) variables))
  357. ((= (length elt) 2)
  358. (push elt vars))))
  359. (setq loc (treepy-replace loc (vconcat (nreverse vars)))))))
  360. (if (treepy-end-p loc)
  361. (let ((node (copy-sequence (treepy-node loc))))
  362. (when variables
  363. (push (cl-coerce variables 'vector)
  364. (cdr node)))
  365. (cl-return node))
  366. (setq loc (treepy-next loc)))))))
  367. (defun ghub--graphql-handle-response (status req)
  368. (let ((buffer (current-buffer)))
  369. (unwind-protect
  370. (progn
  371. (set-buffer-multibyte t)
  372. (let* ((headers (ghub--handle-response-headers status req))
  373. (payload (ghub--handle-response-payload req))
  374. (payload (ghub--handle-response-error status payload req))
  375. (err (plist-get status :error))
  376. (errors (cdr (assq 'errors payload)))
  377. (errors (and errors (cons 'ghub-graphql-error errors))))
  378. (if (or err errors)
  379. (if-let ((errorback (ghub--req-errorback req)))
  380. (funcall errorback (or err errors) headers status req)
  381. (ghub--signal-error (or err errors)))
  382. (ghub--graphql-walk-response req (assq 'data payload)))))
  383. (when (buffer-live-p buffer)
  384. (kill-buffer buffer)))))
  385. (defun ghub--graphql-walk-response (req data)
  386. (let* ((loc (ghub--req-value req))
  387. (loc (if (not loc)
  388. (ghub--alist-zip data)
  389. (setq data (ghub--graphql-narrow-data
  390. data (ghub--graphql-lineage loc)))
  391. (setf (alist-get 'edges data)
  392. (append (alist-get 'edges (treepy-node loc))
  393. (or (alist-get 'edges data)
  394. (error "BUG: Expected new nodes"))))
  395. (treepy-replace loc data))))
  396. (cl-block nil
  397. (while t
  398. (when (eq (car-safe (treepy-node loc)) 'edges)
  399. (setq loc (treepy-up loc))
  400. (pcase-let ((`(,key . ,val) (treepy-node loc)))
  401. (let-alist val
  402. (let* ((cursor (and .pageInfo.hasNextPage
  403. .pageInfo.endCursor))
  404. (until (cdr (assq (intern (format "%s-until" key))
  405. (ghub--graphql-req-until req))))
  406. (nodes (mapcar #'cdar .edges))
  407. (nodes (if until
  408. (seq-take-while
  409. (lambda (node)
  410. (or (string> (cdr (assq 'updatedAt node))
  411. until)
  412. (setq cursor nil)))
  413. nodes)
  414. nodes)))
  415. (if cursor
  416. (progn
  417. (setf (ghub--req-value req) loc)
  418. (ghub--graphql-retrieve req
  419. (ghub--graphql-lineage loc)
  420. cursor)
  421. (cl-return))
  422. (setq loc (treepy-replace loc (cons key nodes))))))))
  423. (if (not (treepy-end-p loc))
  424. (setq loc (treepy-next loc))
  425. (funcall (ghub--req-callback req)
  426. (treepy-root loc))
  427. (cl-return))))))
  428. (defun ghub--graphql-lineage (loc)
  429. (let (lineage)
  430. (while (treepy-up loc)
  431. (push (car (treepy-node loc)) lineage)
  432. (setq loc (treepy-up loc)))
  433. lineage))
  434. (defun ghub--graphql-narrow-data (data lineage)
  435. (let (key)
  436. (while (setq key (pop lineage))
  437. (if (consp (car lineage))
  438. (progn (pop lineage)
  439. (setf data (cadr data)))
  440. (setq data (assq key (cdr data))))))
  441. data)
  442. (defun ghub--graphql-narrow-query (query lineage cursor)
  443. (if (consp (car lineage))
  444. (let* ((child (cddr query))
  445. (alist (cl-coerce (cadr query) 'list))
  446. (single (cdr (assq :singular alist))))
  447. `(,(car single)
  448. ,(vector (list (cadr single) (cdr (car lineage))))
  449. ,@(if (cdr lineage)
  450. (ghub--graphql-narrow-query child (cdr lineage) cursor)
  451. child)))
  452. (let* ((child (or (assq (car lineage) (cdr query))
  453. ;; Alias
  454. (cl-find-if (lambda (c)
  455. (eq (car-safe (car-safe c))
  456. (car lineage)))
  457. query)
  458. ;; Edges
  459. (cl-find-if (lambda (c)
  460. (and (listp c)
  461. (vectorp (cadr c))
  462. (eq (cadr (assq :singular
  463. (cl-coerce (cadr c)
  464. 'list)))
  465. (car lineage))))
  466. (cdr query))
  467. (error "BUG: Failed to narrow query")))
  468. (object (car query))
  469. (args (and (vectorp (cadr query))
  470. (cadr query))))
  471. `(,object
  472. ,@(and args (list args))
  473. ,(cond ((cdr lineage)
  474. (ghub--graphql-narrow-query child (cdr lineage) cursor))
  475. (cursor
  476. `(,(car child)
  477. ,(vconcat `((after ,cursor))
  478. (cadr child))
  479. ,@(cddr child)))
  480. (t
  481. child))))))
  482. (defun ghub--alist-zip (root)
  483. (let ((branchp (lambda (elt) (and (listp elt) (listp (cdr elt)))))
  484. (make-node (lambda (_ children) children)))
  485. (treepy-zipper branchp #'identity make-node root)))
  486. (defun ghub--graphql-set-mode-line (buf string &rest args)
  487. (when (ghub--graphql-req-p buf)
  488. (setq buf (ghub--graphql-req-buffer buf)))
  489. (when (buffer-live-p buf)
  490. (with-current-buffer buf
  491. (setq mode-line-process
  492. (and string (concat " " (apply #'format string args))))
  493. (force-mode-line-update t))))
  494. ;;; _
  495. (provide 'ghub-graphql)
  496. ;;; ghub-graphql.el ends here