dotfiles/emacs/.emacs.d/elpa/forge-20260815.1911/forge-gitlab.el

706 lines
29 KiB
EmacsLisp

;;; forge-gitlab.el --- Gitlab support -*- lexical-binding:t -*-
;; Copyright (C) 2018-2026 Jonas Bernoulli
;; Author: Jonas Bernoulli <emacs.forge@jonas.bernoulli.dev>
;; Maintainer: Jonas Bernoulli <emacs.forge@jonas.bernoulli.dev>
;; SPDX-License-Identifier: GPL-3.0-or-later
;; 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 of the License,
;; 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.
;;
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Code:
(require 'forge-client)
(require 'forge-issue)
(require 'forge-pullreq)
;;; Class
(defclass forge-gitlab-repository (forge-repository)
((issues-url-format :initform "https://%h/%o/%n/issues")
(issue-url-format :initform "https://%h/%o/%n/issues/%i")
(issue-post-url-format :initform "https://%h/%o/%n/issues/%i#note_%I")
(pullreqs-url-format :initform "https://%h/%o/%n/merge_requests")
(pullreq-url-format :initform "https://%h/%o/%n/merge_requests/%i")
(pullreq-post-url-format :initform "https://%h/%o/%n/merge_requests/%i#note_%I")
(commit-url-format :initform "https://%h/%o/%n/commit/%r")
(branch-url-format :initform "https://%h/%o/%n/commits/%r")
(remote-url-format :initform "https://%h/%o/%n")
(blob-url-format :initform "https://%h/%o/%n/-/blob/%r/%f")
(create-issue-url-format :initform "https://%h/%o/%n/issues/new")
(create-pullreq-url-format :initform "https://%h/%o/%n/merge_requests/new")
(pullreq-refspec :initform "+refs/merge-requests/*/head:refs/pullreqs/*")))
;;; Pull
;;;; Repository
(cl-defmethod forge--pull ((repo forge-gitlab-repository)
&optional callback since)
(cl-assert (not (and since (forge-get-repository repo nil :tracked?))))
(setq forge--mode-line-buffer (current-buffer))
(forge--msg repo t nil "Pulling REPO")
(let ((buffer (current-buffer))
(value nil)
(step nil)
(skip (cond ((oref repo selective-p)
'(assignees forks labels issues pullreqs))
((magit-get-boolean "forge.omitExpensive")
'(assignees forks labels)))))
(named-let step (data)
(cond ((not value)
(when data
(setq value data)
(let-alist value
(unless .issues_enabled (cl-pushnew 'issues skip))
(unless .merge_requests_enabled (cl-pushnew 'pullreqs skip)))))
((push (cons step data) value)))
(cl-flet ((fetchp (sym)
(unless (or (memq sym skip)
(assq sym value))
(setq step sym)
t)))
(cond ((not value) (forge--fetch-repository repo #'step))
((fetchp 'assignees) (forge--fetch-assignees repo #'step))
((fetchp 'forks) (forge--fetch-forks repo #'step))
((fetchp 'labels) (forge--fetch-labels repo #'step))
((fetchp 'issues) (forge--fetch-issues repo #'step since))
((fetchp 'pullreqs) (forge--fetch-pullreqs repo #'step since))
(t
(forge--msg repo t t "Pulling REPO")
(forge--msg repo t nil "Storing REPO")
(let-alist value
(closql-with-transaction (forge-db)
(forge--update-repository repo value)
(forge--update-assignees repo .assignees)
(forge--update-labels repo .labels)
(forge--update-issues repo .issues)
(forge--update-pullreqs repo .pullreqs)
(oset repo condition :tracked)))
(forge--msg repo t t "Storing REPO")
(cond ((oref repo selective-p))
(callback (funcall callback))
((forge--maybe-git-fetch repo buffer)))))))))
(cl-defmethod forge--fetch-repository ((repo forge-gitlab-repository) callback)
(forge--glab-get repo "/projects/:project" nil
:callback callback))
(cl-defmethod forge--update-repository ((repo forge-gitlab-repository) data)
(let-alist data
(oset repo created .created_at)
(oset repo updated .last_activity_at)
(oset repo pushed nil)
(oset repo parent .forked_from_project.path_with_namespace)
(oset repo description .description)
(oset repo homepage nil)
(oset repo default-branch .default_branch)
(oset repo archived-p .archived)
(oset repo fork-p (and .forked_from_project.path_with_namespace t))
(oset repo locked-p nil)
(oset repo mirror-p .mirror)
(oset repo private-p (equal .visibility "private"))
(oset repo issues-p .issues_enabled)
(oset repo wiki-p .wiki_enabled)
(oset repo stars .star_count)
(oset repo watchers .star_count)))
;;;; Topics
(cl-defmethod forge--pull-topic ((repo forge-gitlab-repository) _topic
&key callback _errorback)
(forge--pull repo callback)) ; TODO Pull only the one topic.
;;;; Issues
(cl-defmethod forge--fetch-issues ((repo forge-gitlab-repository) callback since)
(letrec
(( cb (let (val cur cnt pos)
(lambda (&optional v)
(cond
((and (not pos) v)
(setq val v)
(setq cur v)
(setq pos 1)
(setq cnt (length val))
(forge--msg nil nil nil "Pulling issue %s/%s" pos cnt)
(forge--fetch-issue-posts repo cur cb))
((setq cur (cdr cur))
(incf pos)
(forge--msg nil nil nil "Pulling issue %s/%s" pos cnt)
(forge--fetch-issue-posts repo cur cb))
(t
(forge--msg repo t t "Pulling REPO issues")
(funcall callback val)))))))
(forge--msg repo t nil "Pulling REPO issues")
(forge--glab-get repo "/projects/:project/issues"
`((per_page . 100)
(order_by . "updated_at")
,@(and$ (or since (oref repo issues-until))
`((updated_after . ,$))))
:unpaginate t
:callback cb)))
(cl-defmethod forge--fetch-issue-posts ((repo forge-gitlab-repository) cur cb)
(forge--glab-get repo
(let-alist (car cur)
(format "/projects/%s/issues/%s/notes" .project_id .iid))
'((per_page . 100))
:unpaginate t
:callback (lambda (value)
(setf (alist-get 'notes (car cur)) value)
(funcall cb))))
(cl-defmethod forge--update-issues ((repo forge-gitlab-repository) data)
(dolist (v data)
(forge--update-issue repo v)))
(cl-defmethod forge--update-issue ((repo forge-gitlab-repository) data)
(closql-with-transaction (forge-db)
(let-alist data
(let* ((issue-id (forge--object-id 'forge-issue repo .iid))
(issue
(forge-issue
:id issue-id
:their-id .iid
:number .iid
:slug (format "#%s" .iid)
:repository (oref repo id)
;; Gitlab doesn't make a distinction between completed
;; and unplanned issues. Treat them all as completed.
:state (pcase-exhaustive .state
("closed" 'completed)
("opened" 'open))
:author .author.username
:title .title
:created .created_at
:updated .updated_at
;; `.closed_at' may be nil even though the issues is
;; closed. In such cases use 1, so that this slot
;; at least can serve as a boolean.
:closed (or .closed_at (and (equal .state "closed") 1))
:locked-p .discussion_locked
:milestone .milestone.iid
:body (forge--sanitize-string .description))))
(closql-insert (forge-db) issue t)
(unless (magit-get-boolean "forge.omitExpensive")
(forge--set-connections repo issue 'assignees .assignees)
(forge--set-connections repo issue 'labels .labels))
(dolist (c .notes)
(let-alist c
(let ((post
(forge-issue-post
:id (forge--object-id issue-id .id)
:issue issue-id
:number .id
:author .author.username
:created .created_at
:updated .updated_at
:body (forge--sanitize-string .body))))
(closql-insert (forge-db) post t))))
(let ((until (oref repo issues-until)))
(when (or (not until) (string> .updated_at until))
(oset repo issues-until .updated_at)))
issue))))
;;;; Pullreqs
(cl-defmethod forge--fetch-pullreqs ((repo forge-gitlab-repository) callback since)
(letrec
(( cb (let (val cur cnt pos)
(lambda (&optional v)
(cond
((and (not pos) v)
(setq val v)
(setq cur v)
(setq pos 1)
(setq cnt (length val))
(forge--msg nil nil nil "Pulling pullreq %s/%s" pos cnt)
(forge--fetch-pullreq-posts repo cur cb))
((not pos)
(forge--msg repo t t "Pulling REPO pullreqs")
(funcall callback val))
((not (assq 'source_project (car cur)))
(forge--fetch-pullreq-source-repo repo cur cb))
((not (assq 'target_project (car cur)))
(forge--fetch-pullreq-target-repo repo cur cb))
((setq cur (cdr cur))
(incf pos)
(forge--msg nil nil nil "Pulling pullreq %s/%s" pos cnt)
(forge--fetch-pullreq-posts repo cur cb))
(t
(forge--msg repo t t "Pulling REPO pullreqs")
(funcall callback val)))))))
(forge--msg repo t nil "Pulling REPO pullreqs")
(forge--glab-get repo "/projects/:project/merge_requests"
`((per_page . 100)
(order_by . "updated_at")
,@(and$ (or since (oref repo pullreqs-until))
`((updated_after . ,$))))
:unpaginate t
:callback cb)))
(cl-defmethod forge--fetch-pullreq-posts
((repo forge-gitlab-repository) cur cb)
(forge--glab-get repo
(let-alist (car cur)
(format "/projects/%s/merge_requests/%s/notes" .target_project_id .iid))
'((per_page . 100))
:unpaginate t
:callback (lambda (value)
(setf (alist-get 'notes (car cur)) value)
(funcall cb))))
(cl-defmethod forge--fetch-pullreq-source-repo
((repo forge-gitlab-repository) cur cb)
;; If the fork no longer exists, then `.source_project_id' is nil.
;; This will lead to difficulties later on but there is nothing we
;; can do about it.
(let-alist (car cur)
(if .source_project_id
(forge--glab-get repo (format "/projects/%s" .source_project_id) nil
:errorback (lambda (_err _headers _status _req)
(setf (alist-get 'source_project (car cur)) nil)
(funcall cb))
:callback (lambda (value)
(setf (alist-get 'source_project (car cur)) value)
(funcall cb)))
(setf (alist-get 'source_project (car cur)) nil)
(funcall cb))))
(cl-defmethod forge--fetch-pullreq-target-repo
((repo forge-gitlab-repository) cur cb)
(let-alist (car cur)
(forge--glab-get repo (format "/projects/%s" .target_project_id) nil
:errorback (lambda (_err _headers _status _req)
(setf (alist-get 'target_project (car cur)) nil)
(funcall cb))
:callback (lambda (value)
(setf (alist-get 'target_project (car cur)) value)
(funcall cb)))))
(cl-defmethod forge--update-pullreqs ((repo forge-gitlab-repository) data)
(dolist (v data)
(forge--update-pullreq repo v)))
(cl-defmethod forge--update-pullreq ((repo forge-gitlab-repository) data)
(closql-with-transaction (forge-db)
(let-alist data
(let* ((pullreq-id (forge--object-id 'forge-pullreq repo .iid))
(pullreq
(forge-pullreq
:id pullreq-id
:their-id .iid
:number .iid
:slug (format "!%s" .iid)
:repository (oref repo id)
:state (pcase-exhaustive .state
("merged" 'merged)
("closed" 'rejected)
("opened" 'open))
:author .author.username
:title .title
:created .created_at
:updated .updated_at
;; `.merged_at' and `.closed_at' may both be nil even
;; though the pullreq is merged or otherwise closed.
;; In such cases use 1, so that these slots at least
;; can serve as booleans.
:closed (or .closed_at
(and (member .state '("closed" "merged")) 1))
:merged (or .merged_at
(and (equal .state "merged") 1))
:draft-p .draft
:locked-p .discussion_locked
:editable-p .allow_maintainer_to_push
:cross-repo-p (not (equal .source_project_id
.target_project_id))
:base-ref .target_branch
:base-rev .diff_refs.start_sha
:base-repo .target_project.path_with_namespace
:head-ref .source_branch
:head-rev .diff_refs.head_sha
:head-user .source_project.owner.username
:head-repo .source_project.path_with_namespace
:milestone .milestone.iid
:body (forge--sanitize-string .description))))
(closql-insert (forge-db) pullreq t)
(unless (magit-get-boolean "forge.omitExpensive")
(forge--set-connections repo pullreq 'assignees .assignees)
(forge--set-connections repo pullreq 'review-requests .reviewers)
(forge--set-connections repo pullreq 'labels .labels))
(dolist (c .notes)
(let-alist c
(let ((post
(forge-pullreq-post
:id (forge--object-id pullreq-id .id)
:pullreq pullreq-id
:number .id
:author .author.username
:created .created_at
:updated .updated_at
:body (forge--sanitize-string .body))))
(closql-insert (forge-db) post t))))
(let ((until (oref repo pullreqs-until)))
(when (or (not until) (string> .updated_at until))
(oset repo pullreqs-until .updated_at)))
pullreq))))
;;;; Other
;; The extend of the documentation for "GET /projects/:id/users" is
;; "Get the users list of a project." I don't know what that means,
;; but it stands to reason that this must at least overlap with the
;; set of users that can be assigned to topics.
(cl-defmethod forge--fetch-assignees ((repo forge-gitlab-repository) callback)
(forge--glab-get repo "/projects/:project/users"
'((per_page . 100))
:unpaginate t
:callback callback))
(cl-defmethod forge--update-assignees ((repo forge-gitlab-repository) data)
(oset repo assignees
(with-slots (id) repo
(mapcar (lambda (row)
(let-alist row
;; For other forges we don't need to store `id'
;; but here we do because that's what has to be
;; used when assigning issues.
(list (forge--object-id id .id)
.username
.name
.id)))
data))))
(cl-defmethod forge--fetch-forks ((repo forge-gitlab-repository) callback)
(forge--glab-get repo "/projects/:project/forks"
'((per_page . 100)
(simple . t))
:unpaginate t
:callback callback))
(cl-defmethod forge--update-forks ((repo forge-gitlab-repository) data)
(oset repo forks
(with-slots (id) repo
(mapcar (lambda (row)
(let-alist row
(nconc (forge--repository-ids
(eieio-object-class repo)
(oref repo githost)
.namespace.path
.path)
(list .namespace.path
.path))))
data))))
(cl-defmethod forge--fetch-labels ((repo forge-gitlab-repository) callback)
(forge--glab-get repo "/projects/:project/labels"
'((per_page . 100))
:unpaginate t
:callback callback))
(cl-defmethod forge--update-labels ((repo forge-gitlab-repository) data)
(oset repo labels
(with-slots (id) repo
(mapcar (lambda (row)
(let-alist row
;; We should use the label's `id' instead of its
;; `name' but a topic's `labels' field is a list
;; of names instead of a list of ids or an alist.
;; As a result of this we cannot recognize when
;; a label is renamed and a topic continues to be
;; tagged with the old label name until it itself
;; is modified somehow. Additionally it leads to
;; name conflicts between group and project
;; labels. See #160. Also see the comment in
;; `forge--set-connections'.
(list (forge--object-id id .name)
.name
(downcase .color)
.description)))
;; For now simply remove one of the duplicates.
(cl-delete-duplicates data
:key (##alist-get 'name %)
:test #'equal)))))
;;;; Notifications
;; The closest to notifications that Gitlab provides are "events" as
;; described at https://docs.gitlab.com/ee/api/events.html. This
;; allows us to see the last events that took place, but that is not
;; good enough because we are mostly interested in events we haven't
;; looked at yet. Gitlab doesn't make a distinction between unread
;; and read events, so this is rather useless and we don't use it for
;; the time being.
;;; Mutations
(cl-defmethod forge--submit-create-issue ((_ forge-gitlab-repository) repo)
(forge--glab-post repo "/projects/:project/issues"
(pcase-let ((`(,title . ,body) (forge--post-buffer-text)))
`((title . ,title)
(description . ,body)))
:callback (forge--post-submit-callback)
:errorback (forge--post-submit-errorback)))
(cl-defmethod forge--submit-create-pullreq ((_ forge-gitlab-repository) base-repo)
(pcase-let* ((`(,title . ,body) (forge--post-buffer-text))
(`(,_base-remote . ,base-branch)
(magit-split-branch-name forge--buffer-base-branch))
(`(,head-remote . ,head-branch)
(magit-split-branch-name forge--buffer-head-branch))
(head-repo (forge-get-repository :stub head-remote)))
(forge--glab-post head-repo "/projects/:project/merge_requests"
`((title . ,(if forge--buffer-draft-p
(concat "Draft: " title)
title))
(description . ,body)
;; ,@(and (not (equal head-remote base-remote))
(target_project_id . ,(oref base-repo forge-id))
(target_branch . ,base-branch)
(source_branch . ,head-branch)
(allow_collaboration . t))
:callback (forge--post-submit-callback)
:errorback (forge--post-submit-errorback))))
(cl-defmethod forge--submit-create-post
((_ forge-gitlab-repository)
(topic forge-topic))
(forge--glab-post topic
(if (forge-issue-p topic)
"/projects/:project/issues/:number/notes"
"/projects/:project/merge_requests/:number/notes")
`((body . ,(string-trim (buffer-str))))
:callback (forge--post-submit-callback)
:errorback (forge--post-submit-errorback)))
(cl-defmethod forge--submit-edit-post
((_ forge-gitlab-repository)
(post forge-post))
(forge--glab-put post
(cl-etypecase post
(forge-pullreq "/projects/:project/merge_requests/:number")
(forge-issue "/projects/:project/issues/:number")
(forge-issue-post "/projects/:project/issues/:topic/notes/:number")
(forge-pullreq-post "/projects/:project/merge_requests/:topic/notes/:number"))
(if (cl-typep post 'forge-topic)
(pcase-let ((`(,title . ,body) (forge--post-buffer-text)))
`((title . ,title)
;; Keep Gitlab from claiming that the user changed
;; the description when that isn't true. The same
;; isn't necessary for the title; for that, Gitlab
;; performs the necessary check itself.
,@(and (not (equal body (oref post body)))
`((description . ,body)))))
`((body . ,(string-trim (buffer-str)))))
:callback (forge--post-submit-callback)
:errorback (forge--post-submit-errorback)))
(cl-defmethod forge--set-topic-field
((_repo forge-gitlab-repository)
(topic forge-topic)
field value)
(forge--glab-put topic
(cl-typecase topic
(forge-pullreq "/projects/:project/merge_requests/:number")
(forge-issue "/projects/:project/issues/:number"))
`((,field . ,(if (and value (listp value)) (vconcat value) value)))
:callback (forge--set-field-callback topic)))
(cl-defmethod forge--set-topic-title
((repo forge-gitlab-repository)
(topic forge-topic)
title)
(forge--set-topic-field repo topic 'title title))
(cl-defmethod forge--set-topic-state
((repo forge-gitlab-repository)
(topic forge-topic)
state)
(forge--set-topic-field repo topic 'state_event
(pcase-exhaustive state
;; Merging isn't done through here.
('completed "close")
('unplanned "close")
('rejected "close")
('open "reopen"))))
(cl-defmethod forge--set-topic-draft
((repo forge-gitlab-repository)
(topic forge-topic)
value)
(forge--mutate-field topic mergeRequestSetDraft
((projectPath (oref repo slug))
(iid (number-to-string (oref topic number)))
(draft value))))
(cl-defmethod forge--set-topic-labels
((repo forge-gitlab-repository)
(topic forge-topic)
labels)
(forge--set-topic-field repo topic 'labels
(string-join labels ",")))
(cl-defmethod forge--set-topic-assignees
((repo forge-gitlab-repository)
(topic forge-topic)
assignees)
(let ((users (mapcar #'cdr (oref repo assignees))))
(cl-typecase topic
(forge-pullreq ; Can only be assigned to a single user.
(forge--set-topic-field repo topic 'assignee_id
(or (caddr (assoc (car assignees) users))
0)))
(forge-issue
(forge--set-topic-field repo topic 'assignee_ids
(or (mapcar (##caddr (assoc % users)) assignees)
0))))))
(cl-defmethod forge--set-topic-review-requests
((repo forge-gitlab-repository)
(topic forge-pullreq)
reviewers)
(let ((users (mapcar #'cdr (oref repo assignees))))
(forge--set-topic-field repo topic 'reviewer_ids
(or (mapcar (##caddr (assoc % users)) reviewers)
0))))
(cl-defmethod forge--delete-comment
((_ forge-gitlab-repository)
(post forge-post))
(forge--glab-delete post
(cl-etypecase post
(forge-pullreq-post
"/projects/:project/merge_requests/:topic/notes/:number")
(forge-issue-post
"/projects/:project/issues/:topic/notes/:number")))
(closql-delete post)
(forge-refresh-buffer))
(cl-defmethod forge--topic-template-files ((repo forge-gitlab-repository)
(_ (subclass forge-issue)))
(forge--topic-template-files-1 repo "md" ".gitlab/issue_templates"))
(cl-defmethod forge--topic-template-files ((repo forge-gitlab-repository)
(_ (subclass forge-pullreq)))
(forge--topic-template-files-1 repo "md" ".gitlab/merge_request_templates"))
(cl-defmethod forge--fork-repository ((repo forge-gitlab-repository) fork _all)
(with-slots (name apihost) repo
(forge--glab-post repo "/projects/:project/fork"
(and (not (equal fork (ghub--username repo)))
`((namespace . ,fork)))
:noerror t)
(ghub-wait (format "/projects/%s%%2F%s" (string-replace "/" "%2F" fork) name)
nil :auth 'forge :host apihost :forge 'gitlab)))
(cl-defmethod forge--merge-pullreq
((_repo forge-gitlab-repository)
(topic forge-topic)
hash method)
(forge--glab-put topic
"/projects/:project/merge_requests/:number/merge"
`((squash . ,(eq method 'squash))
,@(and hash `((sha . ,hash))))))
;;; Wrappers
(cl-defun forge--glab-get (obj resource
&optional params
&key query payload headers
silent unpaginate noerror reader
host callback errorback)
(declare (indent defun))
(ghub-request "GET" (if obj (forge--format-resource obj resource) resource)
params
:forge 'gitlab
:host (or host (oref (forge-get-repository obj) apihost))
:auth 'forge
:query query :payload payload :headers headers
:silent silent :unpaginate unpaginate
:noerror noerror :reader reader
:callback callback
:errorback (or errorback (and callback t))))
(cl-defun forge--glab-put (obj resource
&optional params
&key query payload headers
silent unpaginate noerror reader
host callback errorback)
(declare (indent defun))
(ghub-request "PUT" (if obj (forge--format-resource obj resource) resource)
params
:forge 'gitlab
:host (or host (oref (forge-get-repository obj) apihost))
:auth 'forge
:query query :payload payload :headers headers
:silent silent :unpaginate unpaginate
:noerror noerror :reader reader
:callback callback
:errorback (or errorback (and callback t))))
(cl-defun forge--glab-post (obj resource
&optional params
&key query payload headers
silent unpaginate noerror reader
host callback errorback)
(declare (indent defun))
(ghub-request "POST" (forge--format-resource obj resource)
params
:forge 'gitlab
:host (or host (oref (forge-get-repository obj) apihost))
:auth 'forge
:query query :payload payload :headers headers
:silent silent :unpaginate unpaginate
:noerror noerror :reader reader
:callback callback
:errorback (or errorback (and callback t))))
(cl-defun forge--glab-delete (obj resource
&optional params
&key query payload headers
silent unpaginate noerror reader
host callback errorback)
(declare (indent defun))
(ghub-request "DELETE" (forge--format-resource obj resource)
params
:forge 'gitlab
:host (or host (oref (forge-get-repository obj) apihost))
:auth 'forge
:query query :payload payload :headers headers
:silent silent :unpaginate unpaginate
:noerror noerror :reader reader
:callback callback
:errorback (or errorback (and callback t))))
;;; _
;; Local Variables:
;; read-symbol-shorthands: (
;; ("and$" . "cond-let--and$")
;; ("thread$" . "cond-let--thread$")
;; ("when$" . "cond-let--when$")
;; ("and-let*" . "cond-let--and-let*")
;; ("and-let" . "cond-let--and-let")
;; ("if-let*" . "cond-let--if-let*")
;; ("if-let" . "cond-let--if-let")
;; ("when-let*" . "cond-let--when-let*")
;; ("when-let" . "cond-let--when-let")
;; ("while-let*" . "cond-let--while-let*")
;; ("while-let" . "cond-let--while-let")
;; ("buffer-string" . "buffer-string")
;; ("buffer-str" . "forge--buffer-substring-no-properties"))
;; End:
(provide 'forge-gitlab)
;;; forge-gitlab.el ends here