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.

470 lines
18 KiB

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