forges 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +72 -0
- package/dist/azure-devops/index.d.mts +2 -0
- package/dist/azure-devops/index.mjs +1 -0
- package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
- package/dist/bitbucket/index.d.mts +2 -0
- package/dist/bitbucket/index.mjs +1 -0
- package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
- package/dist/crypto-CQNEyYHD.mjs +156 -0
- package/dist/cursor-origin/index.d.mts +2 -0
- package/dist/cursor-origin/index.mjs +1 -0
- package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
- package/dist/define-B2_4ob2i.d.mts +307 -0
- package/dist/define-D3yEXtKK.mjs +2276 -0
- package/dist/env.d.mts +46 -0
- package/dist/env.mjs +283 -0
- package/dist/fake/index.d.mts +227 -0
- package/dist/fake/index.mjs +1001 -0
- package/dist/fetch-D4pSRvvA.d.mts +1723 -0
- package/dist/forgejo/index.d.mts +2 -0
- package/dist/forgejo/index.mjs +1 -0
- package/dist/forgejo-BCefbHUN.mjs +2005 -0
- package/dist/gitea/index.d.mts +2 -0
- package/dist/gitea/index.mjs +22 -0
- package/dist/gitee/index.d.mts +2 -0
- package/dist/gitee/index.mjs +1 -0
- package/dist/gitee-CA-f7Rs8.mjs +1272 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.mjs +1 -0
- package/dist/github-B0umL8_f.mjs +3259 -0
- package/dist/gitlab/index.d.mts +2 -0
- package/dist/gitlab/index.mjs +1 -0
- package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
- package/dist/graphql-D7uGzZTf.mjs +366 -0
- package/dist/index-BQ6VS31s.d.mts +32 -0
- package/dist/index-C-jz9Mo3.d.mts +16 -0
- package/dist/index-C4fYzmK3.d.mts +102 -0
- package/dist/index-CQiIX4SH.d.mts +27 -0
- package/dist/index-CTj5gQzl.d.mts +35 -0
- package/dist/index-CliBWzOF.d.mts +23 -0
- package/dist/index-Cz_y-t3d.d.mts +36 -0
- package/dist/index-DEt25E1w.d.mts +77 -0
- package/dist/index-DIuS0EOy.d.mts +43 -0
- package/dist/index-uSpsMCrC.d.mts +26 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +13 -0
- package/dist/kit.d.mts +183 -0
- package/dist/kit.mjs +4 -0
- package/dist/provider-CoZ7cCxp.d.mts +1439 -0
- package/dist/provider-Ed1YhkQN.mjs +169 -0
- package/dist/pushin/index.d.mts +2 -0
- package/dist/pushin/index.mjs +1 -0
- package/dist/pushin-CkxgJ2cU.mjs +343 -0
- package/dist/schema/Actor.json +96 -0
- package/dist/schema/AlertSupport.json +49 -0
- package/dist/schema/AuthKind.json +15 -0
- package/dist/schema/Branch.json +31 -0
- package/dist/schema/ChangedFile.json +58 -0
- package/dist/schema/Check.json +169 -0
- package/dist/schema/CheckRef.json +114 -0
- package/dist/schema/CheckState.json +14 -0
- package/dist/schema/ChecksSummary.json +34 -0
- package/dist/schema/CiJob.json +186 -0
- package/dist/schema/CiJobRef.json +132 -0
- package/dist/schema/CiRun.json +234 -0
- package/dist/schema/CiRunRef.json +103 -0
- package/dist/schema/CodeFragment.json +19 -0
- package/dist/schema/CodeMatch.json +128 -0
- package/dist/schema/Collaborator.json +169 -0
- package/dist/schema/Comment.json +336 -0
- package/dist/schema/CommentRef.json +158 -0
- package/dist/schema/Commit.json +307 -0
- package/dist/schema/CommitRef.json +104 -0
- package/dist/schema/CommitSignature.json +118 -0
- package/dist/schema/Comparison.json +355 -0
- package/dist/schema/Cursor.json +21 -0
- package/dist/schema/EventAction.json +33 -0
- package/dist/schema/EventDetail.json +649 -0
- package/dist/schema/EventKind.json +26 -0
- package/dist/schema/FileContent.json +73 -0
- package/dist/schema/FileStatus.json +15 -0
- package/dist/schema/ForgeCapabilities.json +737 -0
- package/dist/schema/ForgeEvent.json +789 -0
- package/dist/schema/ForgeInstance.json +7 -0
- package/dist/schema/ForgeKind.json +32 -0
- package/dist/schema/ForgeWarning.json +49 -0
- package/dist/schema/ForgeWarningCode.json +7 -0
- package/dist/schema/GetManyResult.json +699 -0
- package/dist/schema/Installation.json +131 -0
- package/dist/schema/InstallationToken.json +31 -0
- package/dist/schema/KnownForgeKind.json +19 -0
- package/dist/schema/Label.json +22 -0
- package/dist/schema/MergeMethod.json +14 -0
- package/dist/schema/Milestone.json +40 -0
- package/dist/schema/Notification.json +498 -0
- package/dist/schema/NotificationReason.json +28 -0
- package/dist/schema/NotificationRef.json +60 -0
- package/dist/schema/NotificationSubject.json +376 -0
- package/dist/schema/PerKind.json +49 -0
- package/dist/schema/PullBranches.json +125 -0
- package/dist/schema/PushCommit.json +24 -0
- package/dist/schema/Reaction.json +148 -0
- package/dist/schema/ReactionContent.json +17 -0
- package/dist/schema/ReactionSummary.json +70 -0
- package/dist/schema/Release.json +286 -0
- package/dist/schema/ReleaseAsset.json +169 -0
- package/dist/schema/ReleaseAssetRef.json +138 -0
- package/dist/schema/ReleaseRef.json +107 -0
- package/dist/schema/Repo.json +354 -0
- package/dist/schema/RepoFeatures.json +35 -0
- package/dist/schema/RepoPermissions.json +35 -0
- package/dist/schema/RepoRef.json +77 -0
- package/dist/schema/RepoRole.json +15 -0
- package/dist/schema/RepoVisibility.json +12 -0
- package/dist/schema/Review.json +371 -0
- package/dist/schema/ReviewComment.json +280 -0
- package/dist/schema/ReviewCommentInput.json +34 -0
- package/dist/schema/ReviewRef.json +158 -0
- package/dist/schema/ReviewState.json +15 -0
- package/dist/schema/Reviewer.json +133 -0
- package/dist/schema/SecurityAlert.json +219 -0
- package/dist/schema/SecurityAlertKind.json +14 -0
- package/dist/schema/SecurityAlertRef.json +122 -0
- package/dist/schema/SecuritySeverity.json +14 -0
- package/dist/schema/SourceKind.json +12 -0
- package/dist/schema/SubscriptionState.json +12 -0
- package/dist/schema/Support.json +23 -0
- package/dist/schema/Tag.json +25 -0
- package/dist/schema/TextLimits.json +18 -0
- package/dist/schema/Thread.json +653 -0
- package/dist/schema/ThreadKind.json +14 -0
- package/dist/schema/ThreadRef.json +131 -0
- package/dist/schema/ThreadStack.json +146 -0
- package/dist/schema/ThreadState.json +13 -0
- package/dist/schema/TreeEntry.json +46 -0
- package/dist/schema/TreeEntryType.json +13 -0
- package/dist/schema/UpsertCommentResult.json +355 -0
- package/dist/schema/User.json +123 -0
- package/dist/schema/Webhook.json +188 -0
- package/dist/schema/WebhookDeliveryRecord.json +173 -0
- package/dist/schema/WebhookDeliveryRef.json +135 -0
- package/dist/schema/WebhookRef.json +105 -0
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.mjs +3219 -0
- package/dist/tangled/index.d.mts +2 -0
- package/dist/tangled/index.mjs +1 -0
- package/dist/tangled-ChdPmiv-.mjs +1570 -0
- package/dist/testing/index.d.mts +123 -0
- package/dist/testing/index.mjs +242 -0
- package/dist/webhooks-rn9-8lxT.mjs +70 -0
- package/package.json +115 -3
- package/dist/.gitkeep +0 -0
|
@@ -0,0 +1,2378 @@
|
|
|
1
|
+
import { A as toPage, At as UnresolvedThreadError, B as isNamespaceRef, C as requireIssueOrPull, D as syntheticReview, M as versionAtLeast, Mt as WebhookVerificationError, Nt as soleMergeMethod, O as toDate, Ot as TokenRevokedError, Pt as toMergeError, S as phased, T as resolveToken, Y as reactionContent, b as mergeCommitMessage, bt as InsufficientScopeError, ct as eventAction, dt as fromBase64, f as getManyConcurrently, ft as toFileContent, j as toWarning, jt as UnsupportedOperationError, l as createListing, m as hexColour, n as perKind, o as webUrlFor, pt as AuthenticationRequiredError, r as verb, t as defineForgeProvider, u as degradesToWarning, v as memo, w as requireThread, wt as NotFoundError, x as milestoneId, y as memoBy } from "./define-D3yEXtKK.mjs";
|
|
2
|
+
import { n as headerValue, t as bodyText } from "./crypto-CQNEyYHD.mjs";
|
|
3
|
+
import { i as refEvent, o as verifySharedToken, r as nativeEventsFor, t as eventKindsOf } from "./webhooks-rn9-8lxT.mjs";
|
|
4
|
+
//#region src/gitlab/webhook-events.ts
|
|
5
|
+
/**
|
|
6
|
+
* GitLab subscribes per event with a boolean flag rather than an event name,
|
|
7
|
+
* so the flag names are the native events.
|
|
8
|
+
*/
|
|
9
|
+
const GITLAB_NATIVE_EVENTS = {
|
|
10
|
+
comment: ["note_events", "confidential_note_events"],
|
|
11
|
+
review: ["merge_requests_events"],
|
|
12
|
+
review_comment: ["note_events"],
|
|
13
|
+
state_change: [
|
|
14
|
+
"issues_events",
|
|
15
|
+
"confidential_issues_events",
|
|
16
|
+
"merge_requests_events"
|
|
17
|
+
],
|
|
18
|
+
label: ["issues_events", "merge_requests_events"],
|
|
19
|
+
assignment: ["issues_events", "merge_requests_events"],
|
|
20
|
+
push: ["push_events"],
|
|
21
|
+
ref: ["tag_push_events", "push_events"],
|
|
22
|
+
release: ["releases_events"],
|
|
23
|
+
membership: ["member_events"]
|
|
24
|
+
};
|
|
25
|
+
/** Normalised kinds and actions GitLab deliveries translate into. */
|
|
26
|
+
const GITLAB_WEBHOOK_EVENTS = [
|
|
27
|
+
{
|
|
28
|
+
kind: "comment",
|
|
29
|
+
action: "created"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
kind: "review",
|
|
33
|
+
action: "submitted"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
kind: "review_comment",
|
|
37
|
+
action: "created"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: "state_change",
|
|
41
|
+
action: "opened"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
kind: "state_change",
|
|
45
|
+
action: "closed"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
kind: "state_change",
|
|
49
|
+
action: "reopened"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: "state_change",
|
|
53
|
+
action: "merged"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
kind: "label",
|
|
57
|
+
action: "labelled"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: "label",
|
|
61
|
+
action: "unlabelled"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
kind: "assignment",
|
|
65
|
+
action: "assigned"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
kind: "assignment",
|
|
69
|
+
action: "unassigned"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: "assignment",
|
|
73
|
+
action: "review_requested"
|
|
74
|
+
},
|
|
75
|
+
{ kind: "push" },
|
|
76
|
+
{
|
|
77
|
+
kind: "ref",
|
|
78
|
+
action: "created"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
kind: "ref",
|
|
82
|
+
action: "deleted"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
kind: "release",
|
|
86
|
+
action: "published"
|
|
87
|
+
},
|
|
88
|
+
{ kind: "membership" }
|
|
89
|
+
];
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/gitlab/normalise.ts
|
|
92
|
+
const FORGE = "gitlab";
|
|
93
|
+
function toActor(instance, user) {
|
|
94
|
+
if (!user) return;
|
|
95
|
+
return {
|
|
96
|
+
forge: FORGE,
|
|
97
|
+
instance,
|
|
98
|
+
login: user.username,
|
|
99
|
+
id: String(user.id),
|
|
100
|
+
name: user.name,
|
|
101
|
+
avatarUrl: user.avatar_url ?? void 0,
|
|
102
|
+
url: user.web_url,
|
|
103
|
+
typeRaw: user.bot === void 0 ? void 0 : user.bot ? "bot" : "user",
|
|
104
|
+
isBotHint: user.bot === true || /^(?:project|group)_\d+_bot(?:_|$)/.test(user.username)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Splits a full project path into the namespace (which may contain slashes) and the project name. */
|
|
108
|
+
function toRepoRef(instance, pathWithNamespace, id) {
|
|
109
|
+
const index = pathWithNamespace.lastIndexOf("/");
|
|
110
|
+
return {
|
|
111
|
+
forge: FORGE,
|
|
112
|
+
instance,
|
|
113
|
+
owner: pathWithNamespace.slice(0, index),
|
|
114
|
+
name: pathWithNamespace.slice(index + 1),
|
|
115
|
+
externalId: id === void 0 ? void 0 : String(id)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Reads the project path out of a `/-/`-style web URL, which cross-project listings carry instead of a project path. */
|
|
119
|
+
function repoRefFromWebUrl(instance, webUrl, id) {
|
|
120
|
+
const path = webUrl?.match(/^https?:\/\/[^/]+\/(.+?)\/-\//)?.[1];
|
|
121
|
+
return path ? toRepoRef(instance, path, id) : void 0;
|
|
122
|
+
}
|
|
123
|
+
function toNamespaceRef(instance, fullPath, id) {
|
|
124
|
+
return {
|
|
125
|
+
forge: FORGE,
|
|
126
|
+
instance,
|
|
127
|
+
kind: "namespace",
|
|
128
|
+
owner: fullPath,
|
|
129
|
+
name: "",
|
|
130
|
+
externalId: id === void 0 ? void 0 : String(id)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Project id accepted by every `/projects/:id` route. */
|
|
134
|
+
function projectId(repo) {
|
|
135
|
+
return encodeURIComponent(`${repo.owner}/${repo.name}`);
|
|
136
|
+
}
|
|
137
|
+
function toThreadKind(targetType) {
|
|
138
|
+
switch (targetType) {
|
|
139
|
+
case "Issue": return "issue";
|
|
140
|
+
case "MergeRequest": return "pull_request";
|
|
141
|
+
case "Commit": return "commit";
|
|
142
|
+
default: return "other";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const REASONS = {
|
|
146
|
+
added_approver: "approval_requested",
|
|
147
|
+
approval_required: "approval_requested",
|
|
148
|
+
assigned: "assigned",
|
|
149
|
+
build_failed: "ci_activity",
|
|
150
|
+
directly_addressed: "mention",
|
|
151
|
+
marked: "manual",
|
|
152
|
+
member_access_requested: "access_requested",
|
|
153
|
+
mentioned: "mention",
|
|
154
|
+
merge_train_removed: "merge_blocked",
|
|
155
|
+
review_requested: "review_requested",
|
|
156
|
+
review_submitted: "review_submitted",
|
|
157
|
+
unmergeable: "merge_blocked"
|
|
158
|
+
};
|
|
159
|
+
function toReason(action) {
|
|
160
|
+
return REASONS[action] ?? "unknown";
|
|
161
|
+
}
|
|
162
|
+
function toState(state) {
|
|
163
|
+
switch (state) {
|
|
164
|
+
case "opened": return "open";
|
|
165
|
+
case "closed":
|
|
166
|
+
case "locked": return "closed";
|
|
167
|
+
case "merged": return "merged";
|
|
168
|
+
default: return "unknown";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function toNotification(instance, raw) {
|
|
172
|
+
const repo = raw.project ? toRepoRef(instance, raw.project.path_with_namespace, raw.project.id) : toNamespaceRef(instance, raw.group?.full_path ?? "", raw.group?.id);
|
|
173
|
+
const kind = toThreadKind(raw.target_type);
|
|
174
|
+
const target = raw.target ?? void 0;
|
|
175
|
+
const number = kind === "commit" || target?.iid === void 0 ? target?.id === void 0 ? void 0 : String(target.id) : String(target.iid);
|
|
176
|
+
let subject;
|
|
177
|
+
if (raw.target_type === "Vulnerability") subject = {
|
|
178
|
+
type: "security_alert",
|
|
179
|
+
alert: {
|
|
180
|
+
forge: FORGE,
|
|
181
|
+
instance,
|
|
182
|
+
repo,
|
|
183
|
+
id: number ?? String(raw.id)
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
else if (raw.target_type === "Project" || raw.target_type === "Namespace") subject = {
|
|
187
|
+
type: "repo",
|
|
188
|
+
repo
|
|
189
|
+
};
|
|
190
|
+
else if (kind === "other" && !raw.target?.iid) subject = {
|
|
191
|
+
type: "other",
|
|
192
|
+
typeRaw: raw.target_type,
|
|
193
|
+
repo,
|
|
194
|
+
id: number,
|
|
195
|
+
url: raw.target_url
|
|
196
|
+
};
|
|
197
|
+
else subject = {
|
|
198
|
+
type: "thread",
|
|
199
|
+
thread: {
|
|
200
|
+
forge: FORGE,
|
|
201
|
+
instance,
|
|
202
|
+
repo,
|
|
203
|
+
kind,
|
|
204
|
+
typeRaw: raw.target_type,
|
|
205
|
+
number
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
ref: {
|
|
210
|
+
forge: FORGE,
|
|
211
|
+
instance,
|
|
212
|
+
id: String(raw.id)
|
|
213
|
+
},
|
|
214
|
+
subject,
|
|
215
|
+
subjectTypeRaw: raw.target_type,
|
|
216
|
+
reason: toReason(raw.action_name),
|
|
217
|
+
reasonRaw: raw.action_name,
|
|
218
|
+
unread: raw.state === "pending",
|
|
219
|
+
title: target?.title ?? raw.body ?? "",
|
|
220
|
+
subjectState: toState(target?.state),
|
|
221
|
+
subjectStateRaw: target?.state,
|
|
222
|
+
updatedAt: toDate(raw.updated_at ?? raw.created_at) ?? /* @__PURE__ */ new Date(0),
|
|
223
|
+
url: raw.target_url,
|
|
224
|
+
raw
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const ACCESS_LEVELS$1 = {
|
|
228
|
+
admin: 50,
|
|
229
|
+
maintain: 40,
|
|
230
|
+
push: 30,
|
|
231
|
+
triage: 20,
|
|
232
|
+
pull: 10
|
|
233
|
+
};
|
|
234
|
+
/** GitLab access levels: 50 owner, 40 maintainer, 30 developer, 20 reporter, 10 guest. */
|
|
235
|
+
function toRole(level) {
|
|
236
|
+
return level >= 50 ? "admin" : level >= 40 ? "maintain" : level >= 30 ? "write" : level >= 20 ? "triage" : level >= 10 ? "read" : "none";
|
|
237
|
+
}
|
|
238
|
+
function toLabel(raw) {
|
|
239
|
+
return {
|
|
240
|
+
name: raw.name,
|
|
241
|
+
colour: raw.color?.replace(/^#/, ""),
|
|
242
|
+
description: raw.description ?? void 0
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function toMilestone(raw) {
|
|
246
|
+
return raw ? {
|
|
247
|
+
id: String(raw.id),
|
|
248
|
+
title: raw.title,
|
|
249
|
+
state: raw.state === "closed" ? "closed" : "open",
|
|
250
|
+
description: raw.description ?? void 0,
|
|
251
|
+
dueOn: toDate(raw.due_date),
|
|
252
|
+
url: raw.web_url,
|
|
253
|
+
raw
|
|
254
|
+
} : void 0;
|
|
255
|
+
}
|
|
256
|
+
/** GitLab counts only thumbs up and down on a thread read; the full set needs the award emoji listing. */
|
|
257
|
+
function toVoteReactions(raw) {
|
|
258
|
+
if (raw.upvotes === void 0 && raw.downvotes === void 0) return;
|
|
259
|
+
return {
|
|
260
|
+
total: (raw.upvotes ?? 0) + (raw.downvotes ?? 0),
|
|
261
|
+
counts: {
|
|
262
|
+
...raw.upvotes ? { "+1": raw.upvotes } : {},
|
|
263
|
+
...raw.downvotes ? { "-1": raw.downvotes } : {}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function toRepo(instance, raw) {
|
|
268
|
+
const level = Math.max(raw.permissions?.project_access?.access_level ?? 0, raw.permissions?.group_access?.access_level ?? 0);
|
|
269
|
+
const visibility = raw.visibility === "internal" ? "internal" : raw.visibility === "private" ? "private" : "public";
|
|
270
|
+
return {
|
|
271
|
+
ref: toRepoRef(instance, raw.path_with_namespace, raw.id),
|
|
272
|
+
description: raw.description ?? void 0,
|
|
273
|
+
defaultBranch: raw.default_branch ?? void 0,
|
|
274
|
+
visibility,
|
|
275
|
+
visibilityRaw: raw.visibility,
|
|
276
|
+
isFork: Boolean(raw.forked_from_project),
|
|
277
|
+
isArchived: raw.archived ?? false,
|
|
278
|
+
parent: raw.forked_from_project ? toRepoRef(instance, raw.forked_from_project.path_with_namespace, raw.forked_from_project.id) : void 0,
|
|
279
|
+
topics: raw.topics ?? [],
|
|
280
|
+
url: raw.web_url,
|
|
281
|
+
cloneUrls: raw.http_url_to_repo || raw.ssh_url_to_repo ? {
|
|
282
|
+
https: raw.http_url_to_repo,
|
|
283
|
+
ssh: raw.ssh_url_to_repo
|
|
284
|
+
} : void 0,
|
|
285
|
+
createdAt: toDate(raw.created_at),
|
|
286
|
+
updatedAt: toDate(raw.last_activity_at),
|
|
287
|
+
openIssueCount: raw.open_issues_count,
|
|
288
|
+
owner: toActor(instance, raw.owner),
|
|
289
|
+
licence: raw.license?.key && raw.license.key !== "other" ? raw.license.key : void 0,
|
|
290
|
+
stars: raw.star_count,
|
|
291
|
+
forks: raw.forks_count,
|
|
292
|
+
mergeMethods: raw.merge_method === void 0 ? void 0 : [
|
|
293
|
+
...raw.merge_method === "merge" ? ["merge"] : [],
|
|
294
|
+
...raw.merge_method === "rebase_merge" ? ["rebase_merge"] : [],
|
|
295
|
+
...raw.merge_method === "ff" ? ["fast_forward_only"] : [],
|
|
296
|
+
...raw.squash_option && raw.squash_option !== "never" ? ["squash"] : []
|
|
297
|
+
],
|
|
298
|
+
features: raw.issues_access_level === void 0 && raw.issues_enabled === void 0 ? void 0 : {
|
|
299
|
+
issues: raw.issues_enabled ?? raw.issues_access_level !== "disabled",
|
|
300
|
+
pullRequests: raw.merge_requests_enabled ?? raw.merge_requests_access_level !== "disabled",
|
|
301
|
+
discussions: false,
|
|
302
|
+
wiki: raw.wiki_enabled ?? raw.wiki_access_level !== "disabled",
|
|
303
|
+
projects: false,
|
|
304
|
+
releases: raw.releases_access_level !== "disabled"
|
|
305
|
+
},
|
|
306
|
+
permissions: raw.permissions ? {
|
|
307
|
+
admin: level >= ACCESS_LEVELS$1.admin,
|
|
308
|
+
maintain: level >= ACCESS_LEVELS$1.maintain,
|
|
309
|
+
push: level >= ACCESS_LEVELS$1.push,
|
|
310
|
+
triage: level >= ACCESS_LEVELS$1.triage,
|
|
311
|
+
pull: level >= ACCESS_LEVELS$1.pull
|
|
312
|
+
} : void 0,
|
|
313
|
+
raw
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function toThread(ref, raw) {
|
|
317
|
+
const isMerge = ref.kind === "pull_request";
|
|
318
|
+
const reviewers = (raw.reviewers ?? []).flatMap((user) => {
|
|
319
|
+
const actor = toActor(ref.instance, user);
|
|
320
|
+
return actor ? [{
|
|
321
|
+
actor,
|
|
322
|
+
state: "unknown"
|
|
323
|
+
}] : [];
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
ref: {
|
|
327
|
+
...ref,
|
|
328
|
+
externalId: raw.id === void 0 ? ref.externalId : String(raw.id)
|
|
329
|
+
},
|
|
330
|
+
kind: ref.kind,
|
|
331
|
+
title: raw.title,
|
|
332
|
+
body: raw.description ?? void 0,
|
|
333
|
+
state: toState(raw.state),
|
|
334
|
+
stateRaw: raw.state,
|
|
335
|
+
isDraft: raw.draft ?? false,
|
|
336
|
+
assignees: (raw.assignees ?? []).flatMap((user) => toActor(ref.instance, user) ?? []),
|
|
337
|
+
reviewers,
|
|
338
|
+
lastActivityAt: toDate(raw.updated_at),
|
|
339
|
+
branches: isMerge && raw.source_branch && raw.target_branch ? {
|
|
340
|
+
head: {
|
|
341
|
+
ref: raw.source_branch,
|
|
342
|
+
sha: raw.diff_refs?.head_sha ?? raw.sha,
|
|
343
|
+
repo: raw.source_project_id !== void 0 && raw.source_project_id === raw.target_project_id ? ref.repo : void 0
|
|
344
|
+
},
|
|
345
|
+
base: {
|
|
346
|
+
ref: raw.target_branch,
|
|
347
|
+
sha: raw.diff_refs?.base_sha
|
|
348
|
+
},
|
|
349
|
+
mergeCommitSha: raw.merge_commit_sha ?? raw.squash_commit_sha ?? void 0
|
|
350
|
+
} : void 0,
|
|
351
|
+
author: toActor(ref.instance, raw.author),
|
|
352
|
+
url: raw.web_url,
|
|
353
|
+
createdAt: toDate(raw.created_at),
|
|
354
|
+
updatedAt: toDate(raw.updated_at),
|
|
355
|
+
closedAt: toDate(raw.closed_at),
|
|
356
|
+
labels: (raw.labels ?? []).map((label) => typeof label === "string" ? { name: label } : {
|
|
357
|
+
name: label.name,
|
|
358
|
+
colour: label.color,
|
|
359
|
+
description: label.description ?? void 0
|
|
360
|
+
}),
|
|
361
|
+
locked: raw.discussion_locked ?? void 0,
|
|
362
|
+
commentCount: raw.user_notes_count,
|
|
363
|
+
milestone: toMilestone(raw.milestone),
|
|
364
|
+
reactions: toVoteReactions(raw),
|
|
365
|
+
raw
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function toCommitThread(ref, raw) {
|
|
369
|
+
return {
|
|
370
|
+
ref,
|
|
371
|
+
kind: "commit",
|
|
372
|
+
title: raw.title,
|
|
373
|
+
body: raw.message,
|
|
374
|
+
state: "unknown",
|
|
375
|
+
isDraft: false,
|
|
376
|
+
assignees: [],
|
|
377
|
+
reviewers: [],
|
|
378
|
+
url: raw.web_url,
|
|
379
|
+
createdAt: toDate(raw.created_at),
|
|
380
|
+
lastActivityAt: toDate(raw.created_at),
|
|
381
|
+
labels: [],
|
|
382
|
+
raw
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** System notes are the only record of label, state and assignment changes in the notes API. */
|
|
386
|
+
const SYSTEM_NOTE_KINDS = [
|
|
387
|
+
[/^approved this merge request/i, "review"],
|
|
388
|
+
[/^added \d+ (?:new )?commits?/i, "commit"],
|
|
389
|
+
[/^(?:added|removed) .*label/i, "label"],
|
|
390
|
+
[/^(?:closed|reopened|merged|marked this merge request as|status changed to)/i, "state_change"],
|
|
391
|
+
[/^(?:assigned to|reassigned to|unassigned|requested review from)/i, "assignment"],
|
|
392
|
+
[/^mentioned in /i, "referenced"]
|
|
393
|
+
];
|
|
394
|
+
function toNoteKind(note) {
|
|
395
|
+
if (!note.system) return note.type === "DiffNote" ? "review_comment" : "comment";
|
|
396
|
+
return SYSTEM_NOTE_KINDS.find(([pattern]) => pattern.test(note.body))?.[1] ?? "other";
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* A "mentioned in" system note names the subject that mentioned this thread:
|
|
400
|
+
* `!5` or `acme/widgets!5` for a merge request, `#5` for an issue, or a sha
|
|
401
|
+
* for a commit. The reference is read from that wording, since the note
|
|
402
|
+
* carries no structured source.
|
|
403
|
+
*/
|
|
404
|
+
function crossReference(ref, body) {
|
|
405
|
+
const text = body.match(/^mentioned in (?:merge request|issue|commit) (\S+)/i)?.[1];
|
|
406
|
+
if (!text) return {
|
|
407
|
+
type: "referenced",
|
|
408
|
+
text: body
|
|
409
|
+
};
|
|
410
|
+
const thread = text.match(/^(?:([^!#]+)([!#]))?[!#]?(\d+)$/);
|
|
411
|
+
if (thread) {
|
|
412
|
+
const repo = thread[1] ? toRepoRef(ref.instance, thread[1]) : ref.repo;
|
|
413
|
+
return {
|
|
414
|
+
type: "referenced",
|
|
415
|
+
from: {
|
|
416
|
+
forge: FORGE,
|
|
417
|
+
instance: ref.instance,
|
|
418
|
+
repo,
|
|
419
|
+
kind: text.includes("!") ? "pull_request" : "issue",
|
|
420
|
+
number: thread[3]
|
|
421
|
+
},
|
|
422
|
+
fromRepo: repo,
|
|
423
|
+
text
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return /^[0-9a-f]{7,40}$/i.test(text) ? {
|
|
427
|
+
type: "referenced",
|
|
428
|
+
from: {
|
|
429
|
+
forge: FORGE,
|
|
430
|
+
instance: ref.instance,
|
|
431
|
+
repo: ref.repo,
|
|
432
|
+
sha: text
|
|
433
|
+
},
|
|
434
|
+
fromRepo: ref.repo,
|
|
435
|
+
text
|
|
436
|
+
} : {
|
|
437
|
+
type: "referenced",
|
|
438
|
+
text
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function toNoteEvent(ref, note) {
|
|
442
|
+
const actor = toActor(ref.instance, note.author);
|
|
443
|
+
const kind = toNoteKind(note);
|
|
444
|
+
const who = actor?.login ?? "someone";
|
|
445
|
+
return {
|
|
446
|
+
forge: FORGE,
|
|
447
|
+
instance: ref.instance,
|
|
448
|
+
id: String(note.id),
|
|
449
|
+
kind,
|
|
450
|
+
kindRaw: note.system ? "system" : note.type ?? "Note",
|
|
451
|
+
summary: note.system ? `${who} ${note.body.split("\n")[0]}` : `${who} ${kind === "review_comment" ? "commented on a line" : "commented"}`,
|
|
452
|
+
occurredAt: toDate(note.created_at) ?? /* @__PURE__ */ new Date(0),
|
|
453
|
+
actor,
|
|
454
|
+
repo: ref.repo,
|
|
455
|
+
thread: ref,
|
|
456
|
+
...kind === "referenced" ? { detail: crossReference(ref, note.body) } : {},
|
|
457
|
+
source: "poll",
|
|
458
|
+
payload: note
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function toCommitCommentEvent(ref, comment, index) {
|
|
462
|
+
const actor = toActor(ref.instance, comment.author);
|
|
463
|
+
return {
|
|
464
|
+
forge: FORGE,
|
|
465
|
+
instance: ref.instance,
|
|
466
|
+
id: `${ref.number}:${index}`,
|
|
467
|
+
kind: comment.line_type ? "review_comment" : "comment",
|
|
468
|
+
kindRaw: "CommitComment",
|
|
469
|
+
summary: `${actor?.login ?? "someone"} commented`,
|
|
470
|
+
occurredAt: toDate(comment.created_at) ?? /* @__PURE__ */ new Date(0),
|
|
471
|
+
actor,
|
|
472
|
+
repo: ref.repo,
|
|
473
|
+
thread: ref,
|
|
474
|
+
source: "poll",
|
|
475
|
+
payload: comment
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function toNoteComment(thread, note, discussion) {
|
|
479
|
+
return {
|
|
480
|
+
ref: {
|
|
481
|
+
forge: FORGE,
|
|
482
|
+
instance: thread.instance,
|
|
483
|
+
thread,
|
|
484
|
+
id: String(note.id)
|
|
485
|
+
},
|
|
486
|
+
body: note.body,
|
|
487
|
+
author: toActor(thread.instance, note.author),
|
|
488
|
+
createdAt: toDate(note.created_at),
|
|
489
|
+
updatedAt: toDate(note.updated_at),
|
|
490
|
+
...discussion && note.resolvable && { thread: {
|
|
491
|
+
id: discussion.id,
|
|
492
|
+
resolved: Boolean(note.resolved)
|
|
493
|
+
} },
|
|
494
|
+
raw: note
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** GitLab commit comments carry no id, so they are keyed by creation time and author. */
|
|
498
|
+
function toCommitComment(thread, comment) {
|
|
499
|
+
return {
|
|
500
|
+
ref: {
|
|
501
|
+
forge: FORGE,
|
|
502
|
+
instance: thread.instance,
|
|
503
|
+
thread,
|
|
504
|
+
id: `${comment.created_at ?? ""}:${comment.author?.id ?? ""}`
|
|
505
|
+
},
|
|
506
|
+
body: comment.note,
|
|
507
|
+
author: toActor(thread.instance, comment.author),
|
|
508
|
+
createdAt: toDate(comment.created_at),
|
|
509
|
+
raw: comment
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Pipeline and job statuses. `manual` jobs wait for a person, so they neither pass nor fail. */
|
|
513
|
+
function pipelineState(status) {
|
|
514
|
+
switch (status) {
|
|
515
|
+
case "success": return "success";
|
|
516
|
+
case "failed":
|
|
517
|
+
case "canceled": return "failure";
|
|
518
|
+
case "skipped":
|
|
519
|
+
case "manual": return "neutral";
|
|
520
|
+
case "created":
|
|
521
|
+
case "waiting_for_resource":
|
|
522
|
+
case "preparing":
|
|
523
|
+
case "pending":
|
|
524
|
+
case "running":
|
|
525
|
+
case "scheduled": return "pending";
|
|
526
|
+
default: return "unknown";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/** The merge request's head pipeline, which GitLab reports as one combined status. */
|
|
530
|
+
function toPipelineSummary(pipeline) {
|
|
531
|
+
const state = pipelineState(pipeline.status);
|
|
532
|
+
return {
|
|
533
|
+
state: state === "neutral" ? "pending" : state,
|
|
534
|
+
stateRaw: pipeline.status,
|
|
535
|
+
...pipeline.web_url ? { url: pipeline.web_url } : {}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function toJobCheck(repo, job) {
|
|
539
|
+
const state = pipelineState(job.status);
|
|
540
|
+
return {
|
|
541
|
+
ref: {
|
|
542
|
+
forge: FORGE,
|
|
543
|
+
instance: repo.instance,
|
|
544
|
+
repo,
|
|
545
|
+
id: String(job.id),
|
|
546
|
+
type: "job"
|
|
547
|
+
},
|
|
548
|
+
name: job.stage ? `${job.stage}: ${job.name}` : job.name,
|
|
549
|
+
state: state === "failure" && job.allow_failure ? "neutral" : state,
|
|
550
|
+
stateRaw: job.status,
|
|
551
|
+
url: job.web_url,
|
|
552
|
+
startedAt: toDate(job.started_at),
|
|
553
|
+
completedAt: toDate(job.finished_at),
|
|
554
|
+
raw: job
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** GitLab keys releases by tag and has no drafts or prereleases. */
|
|
558
|
+
function toRelease(repo, raw) {
|
|
559
|
+
return {
|
|
560
|
+
ref: {
|
|
561
|
+
forge: FORGE,
|
|
562
|
+
instance: repo.instance,
|
|
563
|
+
repo,
|
|
564
|
+
id: raw.tag_name,
|
|
565
|
+
tag: raw.tag_name
|
|
566
|
+
},
|
|
567
|
+
name: raw.name ?? void 0,
|
|
568
|
+
tag: raw.tag_name,
|
|
569
|
+
body: raw.description ?? void 0,
|
|
570
|
+
isDraft: false,
|
|
571
|
+
isPrerelease: false,
|
|
572
|
+
author: toActor(repo.instance, raw.author),
|
|
573
|
+
publishedAt: raw.upcoming_release ? void 0 : toDate(raw.released_at),
|
|
574
|
+
createdAt: toDate(raw.created_at),
|
|
575
|
+
url: raw._links?.self,
|
|
576
|
+
assets: raw.assets?.links?.map((link) => ({
|
|
577
|
+
name: link.name,
|
|
578
|
+
url: link.direct_asset_url ?? link.url
|
|
579
|
+
})),
|
|
580
|
+
raw
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const REPORT_KINDS = {
|
|
584
|
+
DEPENDENCY_SCANNING: "dependency",
|
|
585
|
+
CONTAINER_SCANNING: "dependency",
|
|
586
|
+
CLUSTER_IMAGE_SCANNING: "dependency",
|
|
587
|
+
SAST: "code_scanning",
|
|
588
|
+
SECRET_DETECTION: "secret"
|
|
589
|
+
};
|
|
590
|
+
const REPORT_TYPES = {
|
|
591
|
+
dependency: [
|
|
592
|
+
"DEPENDENCY_SCANNING",
|
|
593
|
+
"CONTAINER_SCANNING",
|
|
594
|
+
"CLUSTER_IMAGE_SCANNING"
|
|
595
|
+
],
|
|
596
|
+
code_scanning: ["SAST"],
|
|
597
|
+
secret: ["SECRET_DETECTION"],
|
|
598
|
+
advisory: []
|
|
599
|
+
};
|
|
600
|
+
/** `INFO` findings are informational and carry no severity. */
|
|
601
|
+
function toSeverity(raw) {
|
|
602
|
+
const severity = raw.toLowerCase();
|
|
603
|
+
return severity === "critical" || severity === "high" || severity === "medium" || severity === "low" ? severity : "unknown";
|
|
604
|
+
}
|
|
605
|
+
function toVulnerability(repo, raw) {
|
|
606
|
+
const kind = REPORT_KINDS[raw.reportType] ?? "other";
|
|
607
|
+
const state = raw.state === "DETECTED" || raw.state === "CONFIRMED" ? "open" : raw.state === "RESOLVED" ? "fixed" : raw.state === "DISMISSED" ? "dismissed" : "unknown";
|
|
608
|
+
const id = raw.id.replace(/^gid:\/\/gitlab\/Vulnerability\//, "");
|
|
609
|
+
return {
|
|
610
|
+
ref: {
|
|
611
|
+
forge: FORGE,
|
|
612
|
+
instance: repo.instance,
|
|
613
|
+
repo,
|
|
614
|
+
id,
|
|
615
|
+
kind,
|
|
616
|
+
severityRaw: raw.severity.toLowerCase()
|
|
617
|
+
},
|
|
618
|
+
kind,
|
|
619
|
+
kindRaw: raw.reportType.toLowerCase(),
|
|
620
|
+
severity: toSeverity(raw.severity),
|
|
621
|
+
severityRaw: raw.severity.toLowerCase(),
|
|
622
|
+
state,
|
|
623
|
+
stateRaw: raw.state.toLowerCase(),
|
|
624
|
+
title: raw.title,
|
|
625
|
+
url: raw.webUrl ?? void 0,
|
|
626
|
+
createdAt: toDate(raw.detectedAt),
|
|
627
|
+
updatedAt: toDate(raw.updatedAt),
|
|
628
|
+
dismissedAt: toDate(raw.dismissedAt),
|
|
629
|
+
raw
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function toStatusCheck(repo, raw) {
|
|
633
|
+
const state = pipelineState(raw.status);
|
|
634
|
+
return {
|
|
635
|
+
ref: {
|
|
636
|
+
forge: FORGE,
|
|
637
|
+
instance: repo.instance,
|
|
638
|
+
repo,
|
|
639
|
+
id: String(raw.id),
|
|
640
|
+
type: "status"
|
|
641
|
+
},
|
|
642
|
+
name: raw.name ?? String(raw.id),
|
|
643
|
+
state: state === "failure" && raw.allow_failure ? "neutral" : state,
|
|
644
|
+
stateRaw: raw.status,
|
|
645
|
+
url: raw.target_url ?? void 0,
|
|
646
|
+
startedAt: toDate(raw.created_at),
|
|
647
|
+
completedAt: toDate(raw.finished_at),
|
|
648
|
+
raw
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function toPipeline(repo, raw) {
|
|
652
|
+
return {
|
|
653
|
+
ref: {
|
|
654
|
+
forge: FORGE,
|
|
655
|
+
instance: repo.instance,
|
|
656
|
+
repo,
|
|
657
|
+
id: String(raw.id)
|
|
658
|
+
},
|
|
659
|
+
name: raw.name || raw.source || `pipeline ${raw.id}`,
|
|
660
|
+
state: pipelineState(raw.status),
|
|
661
|
+
stateRaw: raw.status,
|
|
662
|
+
number: raw.iid === void 0 ? void 0 : String(raw.iid),
|
|
663
|
+
eventRaw: raw.source,
|
|
664
|
+
branch: raw.ref,
|
|
665
|
+
sha: raw.sha,
|
|
666
|
+
url: raw.web_url,
|
|
667
|
+
actor: toActor(repo.instance, raw.user),
|
|
668
|
+
createdAt: toDate(raw.created_at),
|
|
669
|
+
startedAt: toDate(raw.started_at),
|
|
670
|
+
completedAt: toDate(raw.finished_at),
|
|
671
|
+
raw
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function toCiJob(run, raw) {
|
|
675
|
+
const state = pipelineState(raw.status);
|
|
676
|
+
return {
|
|
677
|
+
ref: {
|
|
678
|
+
forge: FORGE,
|
|
679
|
+
instance: run.instance,
|
|
680
|
+
repo: run.repo,
|
|
681
|
+
id: String(raw.id),
|
|
682
|
+
run
|
|
683
|
+
},
|
|
684
|
+
name: raw.name,
|
|
685
|
+
state: state === "failure" && raw.allow_failure ? "neutral" : state,
|
|
686
|
+
stateRaw: raw.status,
|
|
687
|
+
stage: raw.stage,
|
|
688
|
+
url: raw.web_url,
|
|
689
|
+
startedAt: toDate(raw.started_at),
|
|
690
|
+
completedAt: toDate(raw.finished_at),
|
|
691
|
+
raw
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function toCommit(repo, raw, files) {
|
|
695
|
+
return {
|
|
696
|
+
ref: {
|
|
697
|
+
forge: FORGE,
|
|
698
|
+
instance: repo.instance,
|
|
699
|
+
repo,
|
|
700
|
+
sha: raw.id
|
|
701
|
+
},
|
|
702
|
+
sha: raw.id,
|
|
703
|
+
message: raw.message ?? raw.title,
|
|
704
|
+
author: {
|
|
705
|
+
name: raw.author_name,
|
|
706
|
+
email: raw.author_email,
|
|
707
|
+
date: toDate(raw.authored_date ?? raw.created_at)
|
|
708
|
+
},
|
|
709
|
+
committer: {
|
|
710
|
+
name: raw.committer_name,
|
|
711
|
+
email: raw.committer_email,
|
|
712
|
+
date: toDate(raw.committed_date)
|
|
713
|
+
},
|
|
714
|
+
parents: raw.parent_ids ?? [],
|
|
715
|
+
url: raw.web_url,
|
|
716
|
+
...raw.stats ? { stats: {
|
|
717
|
+
additions: raw.stats.additions ?? 0,
|
|
718
|
+
deletions: raw.stats.deletions ?? 0,
|
|
719
|
+
total: raw.stats.total
|
|
720
|
+
} } : {},
|
|
721
|
+
...files ? { files } : {},
|
|
722
|
+
raw
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/** GitLab reports a change by three booleans and a unified diff, with no counts. */
|
|
726
|
+
function toChangedFile(raw) {
|
|
727
|
+
const path = raw.new_path ?? raw.old_path ?? "";
|
|
728
|
+
const status = raw.new_file ? "added" : raw.deleted_file ? "removed" : raw.renamed_file ? "renamed" : "modified";
|
|
729
|
+
return {
|
|
730
|
+
path,
|
|
731
|
+
previousPath: raw.renamed_file ? raw.old_path : void 0,
|
|
732
|
+
status,
|
|
733
|
+
statusRaw: status,
|
|
734
|
+
patch: raw.diff
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function toBranch(raw) {
|
|
738
|
+
return {
|
|
739
|
+
name: raw.name,
|
|
740
|
+
sha: raw.commit?.id ?? "",
|
|
741
|
+
isDefault: raw.default,
|
|
742
|
+
isProtected: raw.protected,
|
|
743
|
+
url: raw.web_url,
|
|
744
|
+
raw
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function toTag(raw) {
|
|
748
|
+
return {
|
|
749
|
+
name: raw.name,
|
|
750
|
+
sha: raw.commit?.id ?? raw.target ?? "",
|
|
751
|
+
raw
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function toTreeEntry(raw) {
|
|
755
|
+
return {
|
|
756
|
+
path: raw.path,
|
|
757
|
+
type: raw.mode === "120000" ? "symlink" : raw.type === "tree" ? "directory" : raw.type === "commit" ? "submodule" : "file",
|
|
758
|
+
sha: raw.id,
|
|
759
|
+
mode: raw.mode
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function toWebhook(target, raw) {
|
|
763
|
+
const nativeEvents = Object.entries(raw).filter(([key, value]) => key.endsWith("_events") && value === true).map(([key]) => key);
|
|
764
|
+
return {
|
|
765
|
+
ref: {
|
|
766
|
+
forge: FORGE,
|
|
767
|
+
instance: target.instance,
|
|
768
|
+
target,
|
|
769
|
+
id: String(raw.id)
|
|
770
|
+
},
|
|
771
|
+
url: raw.url,
|
|
772
|
+
events: eventKindsOf(GITLAB_NATIVE_EVENTS, nativeEvents),
|
|
773
|
+
nativeEvents,
|
|
774
|
+
active: !raw.disabled_until,
|
|
775
|
+
createdAt: toDate(raw.created_at),
|
|
776
|
+
raw
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function toWebhookDelivery(hook, raw) {
|
|
780
|
+
const status = Number(raw.response_status) || 0;
|
|
781
|
+
return {
|
|
782
|
+
ref: {
|
|
783
|
+
forge: FORGE,
|
|
784
|
+
instance: hook.instance,
|
|
785
|
+
hook,
|
|
786
|
+
id: String(raw.id)
|
|
787
|
+
},
|
|
788
|
+
event: raw.trigger ?? "",
|
|
789
|
+
status,
|
|
790
|
+
ok: status >= 200 && status < 300,
|
|
791
|
+
deliveredAt: toDate(raw.created_at),
|
|
792
|
+
duration: raw.execution_duration === void 0 ? void 0 : Math.round(raw.execution_duration * 1e3),
|
|
793
|
+
raw
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/gitlab/web.ts
|
|
798
|
+
const SECTIONS = {
|
|
799
|
+
issue: "issues",
|
|
800
|
+
pull_request: "merge_requests",
|
|
801
|
+
commit: "commit"
|
|
802
|
+
};
|
|
803
|
+
const KINDS = new Map(Object.entries(SECTIONS).map(([kind, section]) => [section, kind]));
|
|
804
|
+
const PREFIXES = {
|
|
805
|
+
issue: "#",
|
|
806
|
+
pull_request: "!"
|
|
807
|
+
};
|
|
808
|
+
const encode = (path) => path.split("/").map(encodeURIComponent).join("/");
|
|
809
|
+
const projectPath = (repo) => `/${encode(repo.owner)}/${encodeURIComponent(repo.name)}`;
|
|
810
|
+
function gitlabWeb(origin) {
|
|
811
|
+
return {
|
|
812
|
+
origin,
|
|
813
|
+
repo: (repo) => repo.kind === "namespace" ? `/${encode(repo.owner)}` : projectPath(repo),
|
|
814
|
+
thread: (ref) => ref.repo.kind === "namespace" || !SECTIONS[ref.kind] ? void 0 : `${projectPath(ref.repo)}/-/${SECTIONS[ref.kind]}/${encodeURIComponent(ref.number)}`,
|
|
815
|
+
comment: (ref) => ref.thread.repo.kind === "namespace" || !SECTIONS[ref.thread.kind] ? void 0 : `${projectPath(ref.thread.repo)}/-/${SECTIONS[ref.thread.kind]}/${encodeURIComponent(ref.thread.number)}#note_${ref.id}`,
|
|
816
|
+
release: (ref) => ref.tag ? `${projectPath(ref.repo)}/-/releases/${encodeURIComponent(ref.tag)}` : void 0,
|
|
817
|
+
file: (repo, path, at, line) => `${projectPath(repo)}/-/blob/${encodeURIComponent(at)}/${encode(path)}${line ? `#L${line}` : ""}`,
|
|
818
|
+
compare: (repo, base, head) => `${projectPath(repo)}/-/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`,
|
|
819
|
+
parse: (segments, url, from) => {
|
|
820
|
+
const separator = segments.indexOf("-");
|
|
821
|
+
const path = separator < 0 ? segments : segments.slice(0, separator);
|
|
822
|
+
if (path.length < 2) return;
|
|
823
|
+
const repo = {
|
|
824
|
+
...from,
|
|
825
|
+
owner: path.slice(0, -1).join("/"),
|
|
826
|
+
name: path.at(-1).replace(/\.git$/, "")
|
|
827
|
+
};
|
|
828
|
+
const [section, id] = separator < 0 ? [] : segments.slice(separator + 1);
|
|
829
|
+
if (section === "releases" && id) return {
|
|
830
|
+
repo,
|
|
831
|
+
release: {
|
|
832
|
+
...from,
|
|
833
|
+
repo,
|
|
834
|
+
id,
|
|
835
|
+
tag: id
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
const kind = section ? KINDS.get(section) : void 0;
|
|
839
|
+
if (!kind || !id) return { repo };
|
|
840
|
+
const thread = {
|
|
841
|
+
...from,
|
|
842
|
+
repo,
|
|
843
|
+
kind,
|
|
844
|
+
number: id
|
|
845
|
+
};
|
|
846
|
+
const note = url.hash.startsWith("#note_") ? url.hash.slice(6) : void 0;
|
|
847
|
+
return {
|
|
848
|
+
repo,
|
|
849
|
+
thread,
|
|
850
|
+
...note ? { comment: {
|
|
851
|
+
...from,
|
|
852
|
+
thread,
|
|
853
|
+
id: note
|
|
854
|
+
} } : {}
|
|
855
|
+
};
|
|
856
|
+
},
|
|
857
|
+
reference: (ref, same, expand) => {
|
|
858
|
+
if (ref.kind === "commit") return `${same ? "" : `${ref.repo.owner}/${ref.repo.name}@`}${ref.number.slice(0, 8)}`;
|
|
859
|
+
const prefix = PREFIXES[ref.kind];
|
|
860
|
+
if (!prefix || ref.repo.kind === "namespace") return;
|
|
861
|
+
return `${same ? "" : `${ref.repo.owner}/${ref.repo.name}`}${prefix}${ref.number}${expand ? "+" : ""}`;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/gitlab/webhooks.ts
|
|
867
|
+
const TOKEN_HEADER = "x-gitlab-token";
|
|
868
|
+
const EVENT_HEADER = "x-gitlab-event";
|
|
869
|
+
/**
|
|
870
|
+
* GitLab sends the configured secret verbatim in `X-Gitlab-Token`; there is no
|
|
871
|
+
* body signature to compute.
|
|
872
|
+
*/
|
|
873
|
+
async function verifyGitLabToken(delivery, secret) {
|
|
874
|
+
return verifySharedToken(delivery, secret, TOKEN_HEADER);
|
|
875
|
+
}
|
|
876
|
+
const ZERO_SHA = /^0+$/;
|
|
877
|
+
function repoLevel(instance, payload, who, repo) {
|
|
878
|
+
switch (payload.object_kind ?? payload.event_name) {
|
|
879
|
+
case "push":
|
|
880
|
+
case "tag_push": return refEvent(who, {
|
|
881
|
+
ref: payload.ref ?? "",
|
|
882
|
+
before: payload.before,
|
|
883
|
+
after: payload.after,
|
|
884
|
+
created: Boolean(payload.before && ZERO_SHA.test(payload.before)),
|
|
885
|
+
deleted: Boolean(payload.after && ZERO_SHA.test(payload.after)),
|
|
886
|
+
commits: (payload.commits ?? []).map((commit) => ({
|
|
887
|
+
sha: commit.id,
|
|
888
|
+
message: commit.message,
|
|
889
|
+
author: commit.author?.name,
|
|
890
|
+
url: commit.url
|
|
891
|
+
})),
|
|
892
|
+
commitCount: payload.total_commits_count
|
|
893
|
+
});
|
|
894
|
+
case "release":
|
|
895
|
+
if (payload.action !== "create" || !repo) return;
|
|
896
|
+
return {
|
|
897
|
+
kind: "release",
|
|
898
|
+
action: "published",
|
|
899
|
+
detail: {
|
|
900
|
+
type: "release",
|
|
901
|
+
release: {
|
|
902
|
+
forge: FORGE,
|
|
903
|
+
instance,
|
|
904
|
+
repo,
|
|
905
|
+
id: payload.tag ?? String(payload.id),
|
|
906
|
+
tag: payload.tag
|
|
907
|
+
},
|
|
908
|
+
name: payload.name
|
|
909
|
+
},
|
|
910
|
+
summary: `${who} published ${payload.name ?? payload.tag ?? "a release"}`
|
|
911
|
+
};
|
|
912
|
+
case "project_rename": return {
|
|
913
|
+
kind: "repo",
|
|
914
|
+
action: "renamed",
|
|
915
|
+
detail: {
|
|
916
|
+
type: "repo_renamed",
|
|
917
|
+
from: payload.old_path_with_namespace ?? "",
|
|
918
|
+
to: payload.path_with_namespace ?? ""
|
|
919
|
+
},
|
|
920
|
+
summary: `${who} renamed the project`
|
|
921
|
+
};
|
|
922
|
+
case "project_transfer": return {
|
|
923
|
+
kind: "repo",
|
|
924
|
+
action: "transferred",
|
|
925
|
+
detail: {
|
|
926
|
+
type: "repo_transferred",
|
|
927
|
+
fromOwner: payload.old_path_with_namespace?.split("/").slice(0, -1).join("/"),
|
|
928
|
+
toOwner: payload.path_with_namespace?.split("/").slice(0, -1).join("/")
|
|
929
|
+
},
|
|
930
|
+
summary: `${who} transferred the project`
|
|
931
|
+
};
|
|
932
|
+
case "user_add_to_team":
|
|
933
|
+
case "user_remove_from_team":
|
|
934
|
+
case "user_add_to_group":
|
|
935
|
+
case "user_remove_from_group":
|
|
936
|
+
case "user_access_request_to_project":
|
|
937
|
+
case "user_update_for_team":
|
|
938
|
+
case "user_update_for_group": return {
|
|
939
|
+
kind: "membership",
|
|
940
|
+
action: membershipAction(payload.event_name),
|
|
941
|
+
detail: {
|
|
942
|
+
type: "membership",
|
|
943
|
+
actionRaw: payload.event_name ?? "member",
|
|
944
|
+
member: payload.user_name ? toActor(instance, {
|
|
945
|
+
id: payload.user_id ?? 0,
|
|
946
|
+
username: payload.user_username ?? payload.user_name,
|
|
947
|
+
name: payload.user_name
|
|
948
|
+
}) : void 0
|
|
949
|
+
},
|
|
950
|
+
summary: `${who} changed a membership`
|
|
951
|
+
};
|
|
952
|
+
default: return;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function membershipAction(eventName) {
|
|
956
|
+
if (eventName?.startsWith("user_add_")) return "added";
|
|
957
|
+
if (eventName?.startsWith("user_remove_")) return "removed";
|
|
958
|
+
return eventName?.startsWith("user_update_") ? "edited" : "other";
|
|
959
|
+
}
|
|
960
|
+
function diff(change, key) {
|
|
961
|
+
const previous = change?.previous ?? [];
|
|
962
|
+
const current = change?.current ?? [];
|
|
963
|
+
const before = new Set(previous.map(key));
|
|
964
|
+
const after = new Set(current.map(key));
|
|
965
|
+
return {
|
|
966
|
+
added: current.filter((item) => !before.has(key(item))),
|
|
967
|
+
removed: previous.filter((item) => !after.has(key(item)))
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/** One event per label, assignee or reviewer an `update` delivery adds or removes. */
|
|
971
|
+
function changeEvents(instance, payload, who) {
|
|
972
|
+
const changes = payload.changes;
|
|
973
|
+
if (payload.object_attributes?.action !== "update" || !changes) return [];
|
|
974
|
+
const events = [];
|
|
975
|
+
const labels = diff(changes.labels, (label) => label.title);
|
|
976
|
+
for (const [items, action, verb] of [[
|
|
977
|
+
labels.added,
|
|
978
|
+
"labelled",
|
|
979
|
+
"added"
|
|
980
|
+
], [
|
|
981
|
+
labels.removed,
|
|
982
|
+
"unlabelled",
|
|
983
|
+
"removed"
|
|
984
|
+
]]) for (const { title = "" } of items) events.push({
|
|
985
|
+
kind: "label",
|
|
986
|
+
action,
|
|
987
|
+
detail: {
|
|
988
|
+
type: "label",
|
|
989
|
+
label: title
|
|
990
|
+
},
|
|
991
|
+
summary: `${who} ${verb} label ${title}`.trim()
|
|
992
|
+
});
|
|
993
|
+
const people = (change, added, removed) => {
|
|
994
|
+
const { added: joined, removed: left } = diff(change, (user) => user.username);
|
|
995
|
+
for (const [users, [action, verb]] of [[joined, added], [left, removed]]) for (const user of users) events.push({
|
|
996
|
+
kind: "assignment",
|
|
997
|
+
action,
|
|
998
|
+
detail: {
|
|
999
|
+
type: "assignment",
|
|
1000
|
+
assignee: toActor(instance, user)
|
|
1001
|
+
},
|
|
1002
|
+
summary: `${who} ${verb} ${user.username}`
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
people(changes.assignees, ["assigned", "assigned"], ["unassigned", "unassigned"]);
|
|
1006
|
+
people(changes.reviewers, ["review_requested", "requested review from"], ["other", "removed the review request for"]);
|
|
1007
|
+
return events;
|
|
1008
|
+
}
|
|
1009
|
+
function eventKindFor(payload) {
|
|
1010
|
+
const attributes = payload.object_attributes;
|
|
1011
|
+
switch (payload.object_kind) {
|
|
1012
|
+
case "note": return attributes?.type === "DiffNote" || attributes?.position ? "review_comment" : "comment";
|
|
1013
|
+
case "push": return "commit";
|
|
1014
|
+
case "emoji": return "reaction";
|
|
1015
|
+
case "issue":
|
|
1016
|
+
case "merge_request": switch (attributes?.action) {
|
|
1017
|
+
case "approved":
|
|
1018
|
+
case "approval": return "review";
|
|
1019
|
+
case "open":
|
|
1020
|
+
case "close":
|
|
1021
|
+
case "reopen":
|
|
1022
|
+
case "merge": return "state_change";
|
|
1023
|
+
case "update":
|
|
1024
|
+
if (payload.changes?.labels) return "label";
|
|
1025
|
+
if (payload.changes?.assignees || payload.changes?.reviewers) return "assignment";
|
|
1026
|
+
return "other";
|
|
1027
|
+
default: return "other";
|
|
1028
|
+
}
|
|
1029
|
+
default: return "other";
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function translateGitLabWebhook(instance, delivery) {
|
|
1033
|
+
const event = headerValue(delivery.headers, EVENT_HEADER);
|
|
1034
|
+
if (!event) throw new WebhookVerificationError(`Missing ${EVENT_HEADER} header`, {
|
|
1035
|
+
forge: FORGE,
|
|
1036
|
+
instance
|
|
1037
|
+
});
|
|
1038
|
+
const deliveryId = headerValue(delivery.headers, "x-gitlab-event-uuid") ?? crypto.randomUUID();
|
|
1039
|
+
const payload = JSON.parse(bodyText(delivery.body));
|
|
1040
|
+
const repo = payload.project?.path_with_namespace ? toRepoRef(instance, payload.project.path_with_namespace, payload.project.id) : payload.path_with_namespace ? toRepoRef(instance, payload.path_with_namespace, payload.project_id) : void 0;
|
|
1041
|
+
const user = payload.user?.username ? toActor(instance, {
|
|
1042
|
+
id: payload.user.id ?? 0,
|
|
1043
|
+
...payload.user,
|
|
1044
|
+
username: payload.user.username
|
|
1045
|
+
}) : void 0;
|
|
1046
|
+
const attributes = payload.object_attributes;
|
|
1047
|
+
const subject = payload.object_kind === "note" ? payload.merge_request ? {
|
|
1048
|
+
kind: "pull_request",
|
|
1049
|
+
iid: payload.merge_request.iid
|
|
1050
|
+
} : payload.issue ? {
|
|
1051
|
+
kind: "issue",
|
|
1052
|
+
iid: payload.issue.iid
|
|
1053
|
+
} : void 0 : payload.object_kind === "merge_request" && attributes?.iid !== void 0 ? {
|
|
1054
|
+
kind: "pull_request",
|
|
1055
|
+
iid: attributes.iid
|
|
1056
|
+
} : payload.object_kind === "issue" && attributes?.iid !== void 0 ? {
|
|
1057
|
+
kind: "issue",
|
|
1058
|
+
iid: attributes.iid
|
|
1059
|
+
} : void 0;
|
|
1060
|
+
const thread = repo && subject ? {
|
|
1061
|
+
forge: FORGE,
|
|
1062
|
+
instance,
|
|
1063
|
+
repo,
|
|
1064
|
+
kind: subject.kind,
|
|
1065
|
+
number: String(subject.iid)
|
|
1066
|
+
} : void 0;
|
|
1067
|
+
const level = repoLevel(instance, payload, user?.login ?? payload.user_username ?? "someone", repo);
|
|
1068
|
+
if (level) return [{
|
|
1069
|
+
forge: FORGE,
|
|
1070
|
+
instance,
|
|
1071
|
+
id: deliveryId,
|
|
1072
|
+
kind: level.kind,
|
|
1073
|
+
action: level.action,
|
|
1074
|
+
kindRaw: payload.object_kind ?? payload.event_name ?? event,
|
|
1075
|
+
summary: level.summary,
|
|
1076
|
+
occurredAt: toDate(payload.commits?.at(-1)?.timestamp ?? payload.released_at) ?? /* @__PURE__ */ new Date(),
|
|
1077
|
+
actor: user,
|
|
1078
|
+
repo,
|
|
1079
|
+
detail: level.detail,
|
|
1080
|
+
source: "webhook",
|
|
1081
|
+
payload
|
|
1082
|
+
}];
|
|
1083
|
+
const occurredAt = toDate(attributes?.created_at && payload.object_kind === "note" ? attributes.created_at : attributes?.updated_at) ?? /* @__PURE__ */ new Date();
|
|
1084
|
+
const changes = changeEvents(instance, payload, user?.login ?? "someone");
|
|
1085
|
+
if (changes.length) return changes.map((change, index) => ({
|
|
1086
|
+
forge: FORGE,
|
|
1087
|
+
instance,
|
|
1088
|
+
id: changes.length === 1 ? deliveryId : `${deliveryId}:${index}`,
|
|
1089
|
+
...change,
|
|
1090
|
+
kindRaw: `${payload.object_kind}.update`,
|
|
1091
|
+
actionRaw: "update",
|
|
1092
|
+
occurredAt,
|
|
1093
|
+
actor: user,
|
|
1094
|
+
repo,
|
|
1095
|
+
thread,
|
|
1096
|
+
source: "webhook",
|
|
1097
|
+
payload
|
|
1098
|
+
}));
|
|
1099
|
+
const kind = eventKindFor(payload);
|
|
1100
|
+
const action = attributes?.action;
|
|
1101
|
+
return [{
|
|
1102
|
+
forge: FORGE,
|
|
1103
|
+
instance,
|
|
1104
|
+
id: deliveryId,
|
|
1105
|
+
kind,
|
|
1106
|
+
kindRaw: action ? `${payload.object_kind}.${action}` : payload.object_kind ?? event,
|
|
1107
|
+
action: action === "update" && attributes?.oldrev ? "synchronised" : action === "merge" ? "merged" : eventAction(action),
|
|
1108
|
+
actionRaw: action,
|
|
1109
|
+
detail: kind === "comment" || kind === "review_comment" ? {
|
|
1110
|
+
type: kind,
|
|
1111
|
+
comment: thread && attributes?.id !== void 0 ? {
|
|
1112
|
+
forge: FORGE,
|
|
1113
|
+
instance,
|
|
1114
|
+
thread,
|
|
1115
|
+
id: String(attributes.id)
|
|
1116
|
+
} : void 0,
|
|
1117
|
+
body: attributes?.note
|
|
1118
|
+
} : kind === "state_change" ? {
|
|
1119
|
+
type: "state_change",
|
|
1120
|
+
state: action === "merge" ? "merged" : action === "close" ? "closed" : "open"
|
|
1121
|
+
} : void 0,
|
|
1122
|
+
summary: payload.object_kind === "note" ? `${user?.login ?? "someone"} commented` : `${user?.login ?? "someone"} ${action ? `${action} ` : ""}${payload.object_kind ?? event}`.trim(),
|
|
1123
|
+
occurredAt,
|
|
1124
|
+
actor: user,
|
|
1125
|
+
repo,
|
|
1126
|
+
thread,
|
|
1127
|
+
source: "webhook",
|
|
1128
|
+
payload
|
|
1129
|
+
}];
|
|
1130
|
+
}
|
|
1131
|
+
const gitlabWebhooks = ({ options, instance }) => ({
|
|
1132
|
+
events: GITLAB_WEBHOOK_EVENTS,
|
|
1133
|
+
verify: (delivery) => verifyGitLabToken(delivery, options.webhookSecret),
|
|
1134
|
+
translate: (delivery) => translateGitLabWebhook(instance, delivery)
|
|
1135
|
+
});
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/gitlab/index.ts
|
|
1138
|
+
/** GitLab versions with the webhook event log and resend endpoints. */
|
|
1139
|
+
const GITLAB_HOOK_EVENTS = "17.0";
|
|
1140
|
+
const ISSUE_LIKE = {
|
|
1141
|
+
issue: true,
|
|
1142
|
+
pull_request: true
|
|
1143
|
+
};
|
|
1144
|
+
/** Access level per normalised role, as `POST /projects/:id/members` takes it. */
|
|
1145
|
+
const ACCESS_LEVELS = {
|
|
1146
|
+
admin: 50,
|
|
1147
|
+
maintain: 40,
|
|
1148
|
+
write: 30,
|
|
1149
|
+
triage: 20,
|
|
1150
|
+
read: 10,
|
|
1151
|
+
none: 0
|
|
1152
|
+
};
|
|
1153
|
+
/** GitLab awards emoji by name; these are the names behind the normalised reactions. */
|
|
1154
|
+
const AWARD_EMOJI = {
|
|
1155
|
+
"+1": "thumbsup",
|
|
1156
|
+
"-1": "thumbsdown",
|
|
1157
|
+
"laugh": "laughing",
|
|
1158
|
+
"confused": "confused",
|
|
1159
|
+
"heart": "heart",
|
|
1160
|
+
"hooray": "tada",
|
|
1161
|
+
"rocket": "rocket",
|
|
1162
|
+
"eyes": "eyes"
|
|
1163
|
+
};
|
|
1164
|
+
/** The model's name for each award emoji `forges` writes. */
|
|
1165
|
+
const REACTION_NAMES = Object.fromEntries(Object.entries(AWARD_EMOJI).map(([content, name]) => [name, content]));
|
|
1166
|
+
const VULNERABILITIES_QUERY = `query ProjectVulnerabilities($fullPath: ID!, $first: Int, $after: String, $state: [VulnerabilityState!], $reportType: [VulnerabilityReportType!]) {
|
|
1167
|
+
project(fullPath: $fullPath) {
|
|
1168
|
+
vulnerabilities(first: $first, after: $after, state: $state, reportType: $reportType) {
|
|
1169
|
+
pageInfo { hasNextPage endCursor }
|
|
1170
|
+
nodes { id title severity state reportType detectedAt updatedAt dismissedAt resolvedAt webUrl }
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}`;
|
|
1174
|
+
const PROJECTS_QUERY = `query CodeSearchProjects($ids: [ID!]) {
|
|
1175
|
+
projects(ids: $ids) {
|
|
1176
|
+
nodes { id fullPath }
|
|
1177
|
+
}
|
|
1178
|
+
}`;
|
|
1179
|
+
const BASE_METHODS = {
|
|
1180
|
+
merge: "merge",
|
|
1181
|
+
rebase_merge: "rebase_merge",
|
|
1182
|
+
ff: "fast_forward_only"
|
|
1183
|
+
};
|
|
1184
|
+
function threadPath(ref) {
|
|
1185
|
+
const project = `/projects/${projectId(ref.repo)}`;
|
|
1186
|
+
switch (ref.kind) {
|
|
1187
|
+
case "pull_request": return `${project}/merge_requests/${encodeURIComponent(ref.number)}`;
|
|
1188
|
+
case "commit": return `${project}/repository/commits/${encodeURIComponent(ref.number)}`;
|
|
1189
|
+
default: return `${project}/issues/${encodeURIComponent(ref.number)}`;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const GITLAB = {
|
|
1193
|
+
forge: FORGE,
|
|
1194
|
+
baseUrl: "https://gitlab.com",
|
|
1195
|
+
anonymous: true,
|
|
1196
|
+
apiPath: "/api/v4",
|
|
1197
|
+
headers: { accept: "application/json" },
|
|
1198
|
+
authHeaders: ({ options: { auth } }) => auth?.type === "token" ? async () => ({ authorization: `Bearer ${await resolveToken(auth)}` }) : void 0,
|
|
1199
|
+
setup({ options, instance, baseUrl, origin: context, fetcher }) {
|
|
1200
|
+
/** GitLab serves notes, labels, milestones, members, commit statuses, commit and code search and vulnerabilities only to signed-in users. */
|
|
1201
|
+
const anonymous = options.auth?.type === "anonymous";
|
|
1202
|
+
const web = gitlabWeb(baseUrl.replace(/\/api\/v4$/, ""));
|
|
1203
|
+
/**
|
|
1204
|
+
* GitLab lists pending and done to-dos separately. With `all`, pending
|
|
1205
|
+
* pages are followed by done pages; `cursor.token` marks the switch.
|
|
1206
|
+
*/
|
|
1207
|
+
async function notificationPage(listOptions = {}) {
|
|
1208
|
+
const cursor = listOptions.cursor;
|
|
1209
|
+
const state = cursor?.token === "done" ? "done" : "pending";
|
|
1210
|
+
const result = await fetcher.json(cursor?.nextUrl ?? "/todos", {
|
|
1211
|
+
query: cursor?.nextUrl ? void 0 : {
|
|
1212
|
+
state,
|
|
1213
|
+
per_page: listOptions.perPage
|
|
1214
|
+
},
|
|
1215
|
+
signal: listOptions.signal
|
|
1216
|
+
});
|
|
1217
|
+
const since = listOptions.since?.getTime();
|
|
1218
|
+
const items = (result.data ?? []).map((raw) => toNotification(instance, raw)).filter((item) => since === void 0 || item.updatedAt.getTime() >= since);
|
|
1219
|
+
const phase = cursor?.token ?? "pending";
|
|
1220
|
+
return {
|
|
1221
|
+
items,
|
|
1222
|
+
cursor: result.cursor?.nextUrl ? {
|
|
1223
|
+
...result.cursor,
|
|
1224
|
+
token: phase === "done" ? "done" : void 0
|
|
1225
|
+
} : listOptions.all && phase !== "done" ? { token: "done" } : void 0,
|
|
1226
|
+
notModified: result.notModified
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
/** GitLab assigns by user id, so logins are resolved once each. */
|
|
1230
|
+
/** Award emoji hang off the issue, the merge request or a note, all under the same path. */
|
|
1231
|
+
function awardPath(target) {
|
|
1232
|
+
return "thread" in target ? `${threadPath(requireIssueOrPull(target.thread, context, "react to"))}/notes/${target.id}` : threadPath(requireIssueOrPull(target, context, "react to"));
|
|
1233
|
+
}
|
|
1234
|
+
/** The authenticated username, read once; removing an award needs its id, which is found by owner. */
|
|
1235
|
+
const viewerLogin = memo(() => fetcher.json("/user").then((result) => result.data.username, () => void 0));
|
|
1236
|
+
const userIdByName = memoBy((username) => fetcher.json("/users", { query: { username } }).then(({ data }) => {
|
|
1237
|
+
if (!data[0]) throw new UnsupportedOperationError(`No GitLab user named ${username}`, context);
|
|
1238
|
+
return data[0].id;
|
|
1239
|
+
}));
|
|
1240
|
+
function userId(actor) {
|
|
1241
|
+
if (typeof actor !== "string" && /^\d+$/.test(actor.id)) return Promise.resolve(Number(actor.id));
|
|
1242
|
+
return userIdByName(typeof actor === "string" ? actor : actor.login);
|
|
1243
|
+
}
|
|
1244
|
+
const STATUS_STATES = {
|
|
1245
|
+
pending: "pending",
|
|
1246
|
+
success: "success",
|
|
1247
|
+
failure: "failed",
|
|
1248
|
+
neutral: "success",
|
|
1249
|
+
unknown: "pending"
|
|
1250
|
+
};
|
|
1251
|
+
const PIPELINE_STATES = {
|
|
1252
|
+
pending: "running",
|
|
1253
|
+
success: "success",
|
|
1254
|
+
failure: "failed",
|
|
1255
|
+
neutral: "manual",
|
|
1256
|
+
unknown: void 0
|
|
1257
|
+
};
|
|
1258
|
+
async function setDiscussionResolved(thread, id, resolved, options) {
|
|
1259
|
+
const ref = requireThread(thread, context);
|
|
1260
|
+
await fetcher.raw(`${threadPath(ref)}/discussions/${encodeURIComponent(id)}`, {
|
|
1261
|
+
method: "PUT",
|
|
1262
|
+
query: { resolved },
|
|
1263
|
+
signal: options?.signal
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
const list = createListing(fetcher, "per_page");
|
|
1267
|
+
function milestonesPage(repo, listOptions = {}, includeAncestors) {
|
|
1268
|
+
return list(`${projectPath(repo)}/milestones`, listOptions, (raw) => toMilestone(raw), { query: {
|
|
1269
|
+
state: (listOptions.state ?? "open") === "all" ? void 0 : listOptions.state === "closed" ? "closed" : "active",
|
|
1270
|
+
include_ancestors: includeAncestors
|
|
1271
|
+
} });
|
|
1272
|
+
}
|
|
1273
|
+
async function readUser(path, signal) {
|
|
1274
|
+
const { data } = await fetcher.json(path, { signal });
|
|
1275
|
+
return {
|
|
1276
|
+
...toActor(instance, data),
|
|
1277
|
+
bio: data.bio || void 0,
|
|
1278
|
+
company: data.organization || void 0,
|
|
1279
|
+
location: data.location || void 0,
|
|
1280
|
+
websiteUrl: data.website_url || void 0,
|
|
1281
|
+
createdAt: toDate(data.created_at),
|
|
1282
|
+
followers: data.followers,
|
|
1283
|
+
following: data.following,
|
|
1284
|
+
raw: data
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function projectPath(repo) {
|
|
1288
|
+
return `/projects/${projectId(repo)}`;
|
|
1289
|
+
}
|
|
1290
|
+
/** Project hooks, or group hooks for a namespace ref. */
|
|
1291
|
+
function hooksPath(target) {
|
|
1292
|
+
return isNamespaceRef(target) ? `/groups/${encodeURIComponent(target.externalId ?? target.owner)}/hooks` : `${projectPath(target)}/hooks`;
|
|
1293
|
+
}
|
|
1294
|
+
/** GitLab subscribes with one boolean per event group rather than a list of names. */
|
|
1295
|
+
function hookFlags(input) {
|
|
1296
|
+
return Object.fromEntries(nativeEventsFor(GITLAB_NATIVE_EVENTS, input.events, input.nativeEvents).map((flag) => [flag, true]));
|
|
1297
|
+
}
|
|
1298
|
+
function requireMerge(thread, action) {
|
|
1299
|
+
const ref = requireThread(thread, context);
|
|
1300
|
+
if (ref.kind !== "pull_request") throw new UnsupportedOperationError(`Only merge requests can be ${action}`, context);
|
|
1301
|
+
return ref;
|
|
1302
|
+
}
|
|
1303
|
+
/** GitLab's file endpoint needs an explicit ref, so the project's default branch stands in. */
|
|
1304
|
+
async function defaultBranch(repo, options) {
|
|
1305
|
+
const { data } = await fetcher.json(projectPath(repo), { signal: options?.signal });
|
|
1306
|
+
return data.default_branch ?? "HEAD";
|
|
1307
|
+
}
|
|
1308
|
+
async function get(thread, options) {
|
|
1309
|
+
const ref = requireThread(thread, context);
|
|
1310
|
+
if (ref.kind === "discussion") throw new UnsupportedOperationError("GitLab has no discussion threads", context);
|
|
1311
|
+
if (ref.kind === "commit") return toCommitThread(ref, (await fetcher.json(threadPath(ref), { signal: options?.signal })).data);
|
|
1312
|
+
const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
|
|
1313
|
+
const result = toThread(ref, data);
|
|
1314
|
+
if (data.head_pipeline) result.checks = toPipelineSummary(data.head_pipeline);
|
|
1315
|
+
return result;
|
|
1316
|
+
}
|
|
1317
|
+
const graphqlUrl = `${baseUrl.replace(/\/api\/v4$/, "")}/api/graphql`;
|
|
1318
|
+
async function vulnerabilitiesPage(repo, listOptions = {}) {
|
|
1319
|
+
const state = listOptions.state ?? "open";
|
|
1320
|
+
const { data } = await fetcher.json(graphqlUrl, {
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
json: {
|
|
1323
|
+
operationName: "ProjectVulnerabilities",
|
|
1324
|
+
query: VULNERABILITIES_QUERY,
|
|
1325
|
+
variables: {
|
|
1326
|
+
fullPath: `${repo.owner}/${repo.name}`,
|
|
1327
|
+
first: Math.min(listOptions.perPage ?? 50, 100),
|
|
1328
|
+
after: listOptions.cursor?.token ?? null,
|
|
1329
|
+
state: state === "open" ? ["DETECTED", "CONFIRMED"] : state === "closed" ? ["RESOLVED", "DISMISSED"] : null,
|
|
1330
|
+
reportType: listOptions.kind ? REPORT_TYPES[listOptions.kind] : null
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
signal: listOptions.signal
|
|
1334
|
+
});
|
|
1335
|
+
const vulnerabilities = data.data?.project?.vulnerabilities;
|
|
1336
|
+
if (!vulnerabilities) {
|
|
1337
|
+
const error = new InsufficientScopeError(data.errors?.[0]?.message ?? "The token cannot read vulnerabilities for this project, or its licence has no vulnerability report", 403, "", context);
|
|
1338
|
+
if (listOptions.kind) throw error;
|
|
1339
|
+
return {
|
|
1340
|
+
items: [],
|
|
1341
|
+
warnings: [toWarning("insufficient_scope", error, `${repo.owner}/${repo.name}`)]
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
items: vulnerabilities.nodes.map((node) => toVulnerability(repo, node)),
|
|
1346
|
+
cursor: vulnerabilities.pageInfo.hasNextPage && vulnerabilities.pageInfo.endCursor ? { token: vulnerabilities.pageInfo.endCursor } : void 0
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function releasesPage(repo, listOptions = {}) {
|
|
1350
|
+
return list(`${projectPath(repo)}/releases`, listOptions, (raw) => toRelease(repo, raw));
|
|
1351
|
+
}
|
|
1352
|
+
function requirePublishedRelease(input) {
|
|
1353
|
+
if (input.draft) throw new UnsupportedOperationError("GitLab has no draft releases", context);
|
|
1354
|
+
if (input.prerelease) throw new UnsupportedOperationError("GitLab has no prereleases", context);
|
|
1355
|
+
}
|
|
1356
|
+
async function listPage(repo, query = {}) {
|
|
1357
|
+
if (query.kind === "discussion") return {
|
|
1358
|
+
items: [],
|
|
1359
|
+
warnings: [{
|
|
1360
|
+
code: "kind_unsupported",
|
|
1361
|
+
message: "GitLab has no discussion threads"
|
|
1362
|
+
}]
|
|
1363
|
+
};
|
|
1364
|
+
const warnings = !query.cursor && query.sort === "comments" ? [{
|
|
1365
|
+
code: "sort_unsupported",
|
|
1366
|
+
message: "GitLab cannot sort by comment count; sorted by creation time"
|
|
1367
|
+
}] : [];
|
|
1368
|
+
const state = query.state ?? "open";
|
|
1369
|
+
const kinds = query.kind ? [query.kind] : ["issue", "pull_request"];
|
|
1370
|
+
const page = await phased(kinds.map((kind) => async (cursor) => {
|
|
1371
|
+
const merges = kind === "pull_request";
|
|
1372
|
+
const result = await fetcher.page(`${projectPath(repo)}/${merges ? "merge_requests" : "issues"}`, {
|
|
1373
|
+
query: {
|
|
1374
|
+
state: state === "open" ? "opened" : state === "merged" ? "merged" : state === "closed" && !merges ? "closed" : void 0,
|
|
1375
|
+
labels: query.labels?.join(","),
|
|
1376
|
+
author_username: query.author,
|
|
1377
|
+
assignee_username: query.assignee,
|
|
1378
|
+
updated_after: query.since?.toISOString(),
|
|
1379
|
+
created_after: query.createdAfter?.toISOString(),
|
|
1380
|
+
order_by: query.sort === "updated" ? "updated_at" : "created_at",
|
|
1381
|
+
sort: query.direction ?? "desc",
|
|
1382
|
+
per_page: query.perPage
|
|
1383
|
+
},
|
|
1384
|
+
cursor,
|
|
1385
|
+
signal: query.signal
|
|
1386
|
+
});
|
|
1387
|
+
return toPage(result, (raw) => merges && state === "closed" && raw.state === "opened" ? void 0 : toThread({
|
|
1388
|
+
forge: FORGE,
|
|
1389
|
+
instance,
|
|
1390
|
+
repo,
|
|
1391
|
+
kind,
|
|
1392
|
+
number: String(raw.iid)
|
|
1393
|
+
}, raw));
|
|
1394
|
+
}), query.cursor);
|
|
1395
|
+
return warnings.length ? {
|
|
1396
|
+
...page,
|
|
1397
|
+
warnings
|
|
1398
|
+
} : page;
|
|
1399
|
+
}
|
|
1400
|
+
/** Cross-project search runs on the global issue and merge request listings, which take `search` and `in`. */
|
|
1401
|
+
async function searchThreadsPage(query) {
|
|
1402
|
+
const warnings = [];
|
|
1403
|
+
if (!query.cursor && query.involves) warnings.push({
|
|
1404
|
+
code: "filter_unsupported",
|
|
1405
|
+
message: "GitLab has no involves filter; it was ignored"
|
|
1406
|
+
});
|
|
1407
|
+
if (!query.cursor && query.sort === "comments") warnings.push({
|
|
1408
|
+
code: "sort_unsupported",
|
|
1409
|
+
message: "GitLab cannot sort by comment count; sorted by update time"
|
|
1410
|
+
});
|
|
1411
|
+
const state = query.state ?? "all";
|
|
1412
|
+
const kinds = query.kind ? [query.kind] : ["issue", "pull_request"];
|
|
1413
|
+
const page = await phased(kinds.map((kind) => async (cursor) => {
|
|
1414
|
+
const merges = kind === "pull_request";
|
|
1415
|
+
const base = query.repo ? `${projectPath(query.repo)}/` : "/";
|
|
1416
|
+
const result = await fetcher.page(`${base}${merges ? "merge_requests" : "issues"}`, {
|
|
1417
|
+
query: {
|
|
1418
|
+
scope: query.repo ? void 0 : "all",
|
|
1419
|
+
search: query.text,
|
|
1420
|
+
in: query.text ? "title,description" : void 0,
|
|
1421
|
+
state: state === "open" ? "opened" : state === "closed" ? "closed" : void 0,
|
|
1422
|
+
labels: query.labels?.join(","),
|
|
1423
|
+
author_username: query.author,
|
|
1424
|
+
assignee_username: query.assignee,
|
|
1425
|
+
updated_after: query.since?.toISOString(),
|
|
1426
|
+
order_by: query.sort === "created" ? "created_at" : "updated_at",
|
|
1427
|
+
sort: query.direction ?? "desc",
|
|
1428
|
+
per_page: query.perPage
|
|
1429
|
+
},
|
|
1430
|
+
cursor,
|
|
1431
|
+
signal: query.signal
|
|
1432
|
+
});
|
|
1433
|
+
return toPage(result, (raw) => {
|
|
1434
|
+
const repo = query.repo ?? repoRefFromWebUrl(instance, raw.web_url, merges ? raw.target_project_id : void 0);
|
|
1435
|
+
return repo ? toThread({
|
|
1436
|
+
forge: FORGE,
|
|
1437
|
+
instance,
|
|
1438
|
+
repo,
|
|
1439
|
+
kind,
|
|
1440
|
+
number: String(raw.iid)
|
|
1441
|
+
}, raw) : void 0;
|
|
1442
|
+
});
|
|
1443
|
+
}), query.cursor);
|
|
1444
|
+
return warnings.length ? {
|
|
1445
|
+
...page,
|
|
1446
|
+
warnings: [...warnings, ...page.warnings ?? []]
|
|
1447
|
+
} : page;
|
|
1448
|
+
}
|
|
1449
|
+
async function searchReposPage(query) {
|
|
1450
|
+
const path = query.owner ? `/groups/${encodeURIComponent(query.owner)}/projects` : "/projects";
|
|
1451
|
+
const result = await fetcher.page(path, {
|
|
1452
|
+
query: {
|
|
1453
|
+
search: query.text,
|
|
1454
|
+
order_by: query.sort === "created" ? "created_at" : query.sort === "stars" ? "star_count" : query.sort === "updated" ? "updated_at" : void 0,
|
|
1455
|
+
sort: query.direction,
|
|
1456
|
+
per_page: query.perPage
|
|
1457
|
+
},
|
|
1458
|
+
cursor: query.cursor,
|
|
1459
|
+
signal: query.signal
|
|
1460
|
+
});
|
|
1461
|
+
return toPage(result, (raw) => toRepo(instance, raw));
|
|
1462
|
+
}
|
|
1463
|
+
/** Needs Advanced Search, which no endpoint reports; an instance without it returns an empty page. */
|
|
1464
|
+
async function searchCommitsPage(query) {
|
|
1465
|
+
const unsupported = [
|
|
1466
|
+
"author",
|
|
1467
|
+
"committer",
|
|
1468
|
+
"since",
|
|
1469
|
+
"until",
|
|
1470
|
+
"sort"
|
|
1471
|
+
].filter((field) => query[field] !== void 0);
|
|
1472
|
+
const warnings = unsupported.length ? [{
|
|
1473
|
+
code: "filter_unsupported",
|
|
1474
|
+
message: `GitLab commit search cannot filter or sort by ${unsupported.join(", ")}`
|
|
1475
|
+
}] : [];
|
|
1476
|
+
const result = await fetcher.page(query.repo ? `${projectPath(query.repo)}/search` : "/search", {
|
|
1477
|
+
query: {
|
|
1478
|
+
scope: "commits",
|
|
1479
|
+
search: query.text ?? "",
|
|
1480
|
+
per_page: query.perPage
|
|
1481
|
+
},
|
|
1482
|
+
cursor: query.cursor,
|
|
1483
|
+
signal: query.signal
|
|
1484
|
+
});
|
|
1485
|
+
const repo = query.repo;
|
|
1486
|
+
return toPage(result, (raw) => repo && toCommit(repo, raw), warnings);
|
|
1487
|
+
}
|
|
1488
|
+
/** Projects that group and instance code searches named by id, kept so later pages skip the lookup. */
|
|
1489
|
+
const projectRefs = /* @__PURE__ */ new Map();
|
|
1490
|
+
/** A project the token cannot read costs its blobs and a warning, not the page. */
|
|
1491
|
+
async function resolveProjects(ids, signal, warnings) {
|
|
1492
|
+
if (!ids.length) return;
|
|
1493
|
+
try {
|
|
1494
|
+
const { data } = await fetcher.json(graphqlUrl, {
|
|
1495
|
+
method: "POST",
|
|
1496
|
+
json: {
|
|
1497
|
+
operationName: "CodeSearchProjects",
|
|
1498
|
+
query: PROJECTS_QUERY,
|
|
1499
|
+
variables: { ids: ids.map((id) => `gid://gitlab/Project/${id}`) }
|
|
1500
|
+
},
|
|
1501
|
+
signal
|
|
1502
|
+
});
|
|
1503
|
+
for (const node of data.data?.projects?.nodes ?? []) {
|
|
1504
|
+
const id = Number(node.id.slice(node.id.lastIndexOf("/") + 1));
|
|
1505
|
+
projectRefs.set(id, toRepoRef(instance, node.fullPath, id));
|
|
1506
|
+
}
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
if (!degradesToWarning(error)) throw error;
|
|
1509
|
+
warnings.push(...ids.map((id) => toWarning("record_unreachable", error, `project ${id}`)));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
for (const id of ids.filter((id) => !projectRefs.has(id))) warnings.push({
|
|
1513
|
+
code: "record_unreachable",
|
|
1514
|
+
message: "GitLab returned no project for this id",
|
|
1515
|
+
subject: `project ${id}`
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/** Any tier searches a project; a group or the instance needs Advanced Search or exact code search. */
|
|
1519
|
+
async function searchCodePage(query) {
|
|
1520
|
+
const path = query.repo ? `${projectPath(query.repo)}/search` : query.owner ? `/groups/${encodeURIComponent(query.owner)}/search` : "/search";
|
|
1521
|
+
const result = await fetcher.page(path, {
|
|
1522
|
+
query: {
|
|
1523
|
+
scope: "blobs",
|
|
1524
|
+
search: query.text ?? "",
|
|
1525
|
+
per_page: query.perPage
|
|
1526
|
+
},
|
|
1527
|
+
cursor: query.cursor,
|
|
1528
|
+
signal: query.signal
|
|
1529
|
+
});
|
|
1530
|
+
const warnings = [];
|
|
1531
|
+
await resolveProjects(query.repo ? [] : [...new Set((result.data ?? []).map((raw) => raw.project_id))].filter((id) => !projectRefs.has(id)), query.signal, warnings);
|
|
1532
|
+
/** GitLab returns one blob per excerpt; basic search lists a file again, with the same first excerpt, when its name matches too. */
|
|
1533
|
+
const files = /* @__PURE__ */ new Map();
|
|
1534
|
+
for (const blob of result.data ?? []) {
|
|
1535
|
+
const key = `${blob.project_id}:${blob.ref}:${blob.path}`;
|
|
1536
|
+
const excerpts = files.get(key);
|
|
1537
|
+
if (!excerpts) files.set(key, [blob]);
|
|
1538
|
+
else if (!excerpts.some((seen) => seen.startline === blob.startline && seen.data === blob.data)) excerpts.push(blob);
|
|
1539
|
+
}
|
|
1540
|
+
return toPage({
|
|
1541
|
+
...result,
|
|
1542
|
+
data: [...files.values()]
|
|
1543
|
+
}, (excerpts) => {
|
|
1544
|
+
const [first] = excerpts;
|
|
1545
|
+
const repo = query.repo ?? projectRefs.get(first.project_id);
|
|
1546
|
+
if (!repo) return;
|
|
1547
|
+
const url = webUrlFor(web, { file: {
|
|
1548
|
+
repo,
|
|
1549
|
+
path: first.path,
|
|
1550
|
+
at: first.ref,
|
|
1551
|
+
line: first.startline
|
|
1552
|
+
} });
|
|
1553
|
+
return {
|
|
1554
|
+
repo,
|
|
1555
|
+
path: first.path,
|
|
1556
|
+
ref: first.ref,
|
|
1557
|
+
fragments: excerpts.map((raw) => ({
|
|
1558
|
+
text: raw.data,
|
|
1559
|
+
line: raw.startline
|
|
1560
|
+
})),
|
|
1561
|
+
url,
|
|
1562
|
+
raw: excerpts
|
|
1563
|
+
};
|
|
1564
|
+
}, warnings);
|
|
1565
|
+
}
|
|
1566
|
+
async function setSubscribed(thread, subscribed, options) {
|
|
1567
|
+
const ref = requireIssueOrPull(thread, context, subscribed ? "subscribe to" : "unsubscribe from");
|
|
1568
|
+
await fetcher.raw(`${threadPath(ref)}/${subscribed ? "subscribe" : "unsubscribe"}`, {
|
|
1569
|
+
method: "POST",
|
|
1570
|
+
signal: options?.signal
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async function unsubscribe(_ref, writeOptions = {}) {
|
|
1574
|
+
if (!writeOptions.thread) throw new UnresolvedThreadError("GitLab cannot look up a to-do by id; pass { thread } to unsubscribe", context);
|
|
1575
|
+
const thread = requireThread(writeOptions.thread, context);
|
|
1576
|
+
if (thread.kind !== "issue" && thread.kind !== "pull_request") throw new UnsupportedOperationError(`GitLab cannot unsubscribe from a ${thread.kind}`, context);
|
|
1577
|
+
await fetcher.raw(`${threadPath(thread)}/unsubscribe`, {
|
|
1578
|
+
method: "POST",
|
|
1579
|
+
signal: writeOptions.signal
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
async function setState(thread, stateEvent, options) {
|
|
1583
|
+
await fetcher.raw(threadPath(requireIssueOrPull(thread, context, stateEvent)), {
|
|
1584
|
+
method: "PUT",
|
|
1585
|
+
json: { state_event: stateEvent },
|
|
1586
|
+
signal: options?.signal
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async function resolveMergeMethod(ref, requested, signal) {
|
|
1590
|
+
if (requested === "squash") return requested;
|
|
1591
|
+
const { data: project } = await fetcher.json(`/projects/${projectId(ref.repo)}`, { signal });
|
|
1592
|
+
const base = BASE_METHODS[project.merge_method ?? "merge"];
|
|
1593
|
+
const squash = project.squash_option ?? "default_off";
|
|
1594
|
+
if (requested) {
|
|
1595
|
+
if (requested !== base || squash === "always") throw new UnsupportedOperationError(`This project merges with ${squash === "always" ? "squash" : base}; GitLab cannot override it per merge request`, context);
|
|
1596
|
+
return requested;
|
|
1597
|
+
}
|
|
1598
|
+
return soleMergeMethod({
|
|
1599
|
+
[base]: squash !== "always",
|
|
1600
|
+
squash: squash !== "never"
|
|
1601
|
+
}, context);
|
|
1602
|
+
}
|
|
1603
|
+
/** GitLab has approvals, not reviews: one synthesised review per approver, with no comments to hang on it. */
|
|
1604
|
+
function toApprovalReviews(ref, approvals) {
|
|
1605
|
+
return (approvals.approved_by ?? []).map(({ user }) => syntheticReview(ref, `approval:${user?.username ?? user?.id ?? ""}`, "approved", {
|
|
1606
|
+
author: toActor(instance, user),
|
|
1607
|
+
stateRaw: "approved",
|
|
1608
|
+
raw: user
|
|
1609
|
+
}));
|
|
1610
|
+
}
|
|
1611
|
+
async function approve(thread, body, options) {
|
|
1612
|
+
const ref = requireThread(thread, context);
|
|
1613
|
+
if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests can be approved", context);
|
|
1614
|
+
if (body) throw new UnsupportedOperationError("GitLab approvals carry no body; leave a comment instead", context);
|
|
1615
|
+
const { data } = await fetcher.json(`${threadPath(ref)}/approve`, {
|
|
1616
|
+
method: "POST",
|
|
1617
|
+
signal: options?.signal,
|
|
1618
|
+
mapError: (error) => error instanceof TokenRevokedError ? new InsufficientScopeError("Not allowed to approve this merge request", 401, error.body, {
|
|
1619
|
+
...context,
|
|
1620
|
+
url: error.url,
|
|
1621
|
+
method: error.method
|
|
1622
|
+
}, { cause: error }) : error
|
|
1623
|
+
});
|
|
1624
|
+
return data;
|
|
1625
|
+
}
|
|
1626
|
+
async function merge(thread, mergeOptions = {}, hooks = {}) {
|
|
1627
|
+
const ref = requireThread(thread, context);
|
|
1628
|
+
if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests can be merged", context);
|
|
1629
|
+
const method = await resolveMergeMethod(ref, mergeOptions.method, mergeOptions.signal);
|
|
1630
|
+
const message = mergeCommitMessage(mergeOptions);
|
|
1631
|
+
await hooks.beforeMerge?.();
|
|
1632
|
+
await fetcher.raw(`${threadPath(ref)}/merge`, {
|
|
1633
|
+
method: "PUT",
|
|
1634
|
+
json: {
|
|
1635
|
+
sha: mergeOptions.sha,
|
|
1636
|
+
...message ? { [method === "squash" ? "squash_commit_message" : "merge_commit_message"]: message } : {},
|
|
1637
|
+
squash: method === "squash",
|
|
1638
|
+
merge_when_pipeline_succeeds: mergeOptions.whenChecksPass ?? false
|
|
1639
|
+
},
|
|
1640
|
+
mapError: toMergeError,
|
|
1641
|
+
signal: mergeOptions.signal
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
traits: {
|
|
1646
|
+
eventKinds: "heuristic",
|
|
1647
|
+
authKinds: ["token", "anonymous"],
|
|
1648
|
+
limits: {
|
|
1649
|
+
bodyLength: 1048576,
|
|
1650
|
+
commentLength: 1e6,
|
|
1651
|
+
labelLength: 255
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
probeVersion: async () => (await fetcher.json("/version")).data.version,
|
|
1655
|
+
web,
|
|
1656
|
+
webhooks: {
|
|
1657
|
+
listPage: verb(true, (target, listOptions = {}) => list(hooksPath(target), listOptions, (raw) => toWebhook(target, raw))),
|
|
1658
|
+
create: verb(true, async (target, input, options) => toWebhook(target, (await fetcher.json(hooksPath(target), {
|
|
1659
|
+
method: "POST",
|
|
1660
|
+
json: {
|
|
1661
|
+
url: input.url,
|
|
1662
|
+
token: input.secret,
|
|
1663
|
+
...hookFlags(input)
|
|
1664
|
+
},
|
|
1665
|
+
signal: options?.signal
|
|
1666
|
+
})).data)),
|
|
1667
|
+
update: verb(true, async (ref, update, options) => toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${ref.id}`, {
|
|
1668
|
+
method: "PUT",
|
|
1669
|
+
json: {
|
|
1670
|
+
...update.url ? { url: update.url } : {},
|
|
1671
|
+
...update.secret ? { token: update.secret } : {},
|
|
1672
|
+
...hookFlags(update)
|
|
1673
|
+
},
|
|
1674
|
+
signal: options?.signal
|
|
1675
|
+
})).data)),
|
|
1676
|
+
delete: verb(true, async (ref, options) => {
|
|
1677
|
+
await fetcher.raw(`${hooksPath(ref.target)}/${ref.id}`, {
|
|
1678
|
+
method: "DELETE",
|
|
1679
|
+
signal: options?.signal
|
|
1680
|
+
});
|
|
1681
|
+
}),
|
|
1682
|
+
rotateSecret: verb(true, async (ref, secret, options) => toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${ref.id}`, {
|
|
1683
|
+
method: "PUT",
|
|
1684
|
+
json: { token: secret },
|
|
1685
|
+
signal: options?.signal
|
|
1686
|
+
})).data)),
|
|
1687
|
+
deliveriesPage: verb(({ version }) => versionAtLeast(version, GITLAB_HOOK_EVENTS), (ref, listOptions = {}) => list(`${hooksPath(ref.target)}/${ref.id}/events`, listOptions, (raw) => toWebhookDelivery(ref, raw))),
|
|
1688
|
+
redeliver: verb(({ version }) => versionAtLeast(version, GITLAB_HOOK_EVENTS), async (ref, options) => {
|
|
1689
|
+
await fetcher.raw(`${hooksPath(ref.hook.target)}/${ref.hook.id}/events/${ref.id}/resend`, {
|
|
1690
|
+
method: "POST",
|
|
1691
|
+
signal: options?.signal
|
|
1692
|
+
});
|
|
1693
|
+
})
|
|
1694
|
+
},
|
|
1695
|
+
scopes: gitlabScopesFor,
|
|
1696
|
+
users: {
|
|
1697
|
+
get: verb(true, async (login, options) => {
|
|
1698
|
+
const { data: matches } = await fetcher.json("/users", {
|
|
1699
|
+
query: { username: login },
|
|
1700
|
+
signal: options?.signal
|
|
1701
|
+
});
|
|
1702
|
+
const [match] = matches ?? [];
|
|
1703
|
+
if (!match) throw new NotFoundError(`No GitLab user named ${login}`, 404, "", context);
|
|
1704
|
+
try {
|
|
1705
|
+
return await readUser(`/users/${match.id}`, options?.signal);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
if (error instanceof InsufficientScopeError || error instanceof AuthenticationRequiredError) return {
|
|
1708
|
+
...toActor(instance, match),
|
|
1709
|
+
raw: match
|
|
1710
|
+
};
|
|
1711
|
+
throw error;
|
|
1712
|
+
}
|
|
1713
|
+
}),
|
|
1714
|
+
me: verb(true, (options) => readUser("/user", options?.signal))
|
|
1715
|
+
},
|
|
1716
|
+
repos: {
|
|
1717
|
+
get: verb(true, async (ref, options) => {
|
|
1718
|
+
const query = options?.licence ? { license: true } : void 0;
|
|
1719
|
+
return toRepo(instance, (await fetcher.json(projectPath(ref), {
|
|
1720
|
+
query,
|
|
1721
|
+
signal: options?.signal
|
|
1722
|
+
})).data);
|
|
1723
|
+
}),
|
|
1724
|
+
listPage: verb(true, (listOptions = {}) => list("/projects", listOptions, (raw) => toRepo(instance, raw), { query: { membership: true } })),
|
|
1725
|
+
labelsPage: verb(!anonymous, (repo, listOptions = {}) => list(`${projectPath(repo)}/labels`, listOptions, toLabel)),
|
|
1726
|
+
createLabel: verb(true, async (repo, label, options) => toLabel((await fetcher.json(`${projectPath(repo)}/labels`, {
|
|
1727
|
+
method: "POST",
|
|
1728
|
+
json: {
|
|
1729
|
+
name: label.name,
|
|
1730
|
+
color: hexColour(label.colour, "#"),
|
|
1731
|
+
description: label.description
|
|
1732
|
+
},
|
|
1733
|
+
signal: options?.signal
|
|
1734
|
+
})).data)),
|
|
1735
|
+
milestonesPage: verb(!anonymous, milestonesPage),
|
|
1736
|
+
collaboratorsPage: verb(!anonymous, (repo, listOptions = {}) => list(`${projectPath(repo)}/members/all`, listOptions, (raw) => ({
|
|
1737
|
+
actor: toActor(instance, raw),
|
|
1738
|
+
role: toRole(raw.access_level),
|
|
1739
|
+
roleRaw: String(raw.access_level),
|
|
1740
|
+
raw
|
|
1741
|
+
}))),
|
|
1742
|
+
permissionFor: verb(!anonymous, async (repo, actor, options) => {
|
|
1743
|
+
try {
|
|
1744
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/members/all/${await userId(actor)}`, { signal: options?.signal });
|
|
1745
|
+
return toRole(data.access_level);
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
if (error instanceof NotFoundError) return "none";
|
|
1748
|
+
throw error;
|
|
1749
|
+
}
|
|
1750
|
+
}),
|
|
1751
|
+
addCollaborator: verb(true, async (repo, actor, role, options) => {
|
|
1752
|
+
await fetcher.raw(`${projectPath(repo)}/members`, {
|
|
1753
|
+
method: "POST",
|
|
1754
|
+
json: {
|
|
1755
|
+
user_id: await userId(actor),
|
|
1756
|
+
access_level: ACCESS_LEVELS[role]
|
|
1757
|
+
},
|
|
1758
|
+
signal: options?.signal
|
|
1759
|
+
});
|
|
1760
|
+
}),
|
|
1761
|
+
assignableUsersPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/users`, listOptions, (raw) => toActor(instance, raw))),
|
|
1762
|
+
reviewerCandidatesPage: verb("emulated", (thread, listOptions = {}) => list(`${projectPath(requireThread(thread, context).repo)}/users`, listOptions, (raw) => toActor(instance, raw)))
|
|
1763
|
+
},
|
|
1764
|
+
notifications: {
|
|
1765
|
+
listPage: verb(true, notificationPage),
|
|
1766
|
+
markDone: verb(true, async (ref, options) => {
|
|
1767
|
+
await fetcher.raw(`/todos/${ref.id}/mark_as_done`, {
|
|
1768
|
+
method: "POST",
|
|
1769
|
+
signal: options?.signal
|
|
1770
|
+
});
|
|
1771
|
+
}),
|
|
1772
|
+
unsubscribe: verb(true, unsubscribe),
|
|
1773
|
+
markAllDone: verb(true, async (bulk = {}) => {
|
|
1774
|
+
if (bulk.repo || bulk.before) throw new UnsupportedOperationError("GitLab marks every to-do done at once; repo and before filters are not supported", context);
|
|
1775
|
+
await fetcher.raw("/todos/mark_as_done", {
|
|
1776
|
+
method: "POST",
|
|
1777
|
+
signal: bulk.signal
|
|
1778
|
+
});
|
|
1779
|
+
}),
|
|
1780
|
+
unreadCount: verb(true, async (options) => {
|
|
1781
|
+
const total = (await fetcher.raw("/todos", {
|
|
1782
|
+
query: {
|
|
1783
|
+
state: "pending",
|
|
1784
|
+
per_page: 1
|
|
1785
|
+
},
|
|
1786
|
+
signal: options?.signal
|
|
1787
|
+
})).headers.get("x-total");
|
|
1788
|
+
if (total === null) throw new UnsupportedOperationError("GitLab omitted X-Total; the to-do count is too large to report", context);
|
|
1789
|
+
return Number(total);
|
|
1790
|
+
})
|
|
1791
|
+
},
|
|
1792
|
+
contents: {
|
|
1793
|
+
file: verb(true, async (repo, path, fileOptions = {}) => {
|
|
1794
|
+
const ref = fileOptions.ref ?? await defaultBranch(repo, fileOptions);
|
|
1795
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/repository/files/${encodeURIComponent(path.replace(/^\//, ""))}`, {
|
|
1796
|
+
query: { ref },
|
|
1797
|
+
signal: fileOptions.signal
|
|
1798
|
+
});
|
|
1799
|
+
const file = {
|
|
1800
|
+
path: data.file_path,
|
|
1801
|
+
sha: data.blob_id,
|
|
1802
|
+
size: data.size
|
|
1803
|
+
};
|
|
1804
|
+
return toFileContent(fromBase64(data.content ?? ""), file, fileOptions, context);
|
|
1805
|
+
}),
|
|
1806
|
+
treePage: verb(true, (repo, treeOptions = {}) => list(`${projectPath(repo)}/repository/tree`, treeOptions, toTreeEntry, { query: {
|
|
1807
|
+
ref: treeOptions.ref,
|
|
1808
|
+
path: treeOptions.path,
|
|
1809
|
+
recursive: treeOptions.recursive ? "true" : void 0
|
|
1810
|
+
} })),
|
|
1811
|
+
branchesPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/repository/branches`, listOptions, toBranch)),
|
|
1812
|
+
tagsPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/repository/tags`, listOptions, toTag)),
|
|
1813
|
+
resolveRef: verb(true, async (repo, ref, options) => (await fetcher.json(`${projectPath(repo)}/repository/commits/${encodeURIComponent(ref)}`, { signal: options?.signal })).data.id),
|
|
1814
|
+
commitsPage: verb(true, (repo, query = {}) => list(`${projectPath(repo)}/repository/commits`, query, (raw) => toCommit(repo, raw), { query: {
|
|
1815
|
+
ref_name: query.ref,
|
|
1816
|
+
path: query.path,
|
|
1817
|
+
author: query.author,
|
|
1818
|
+
since: query.since?.toISOString(),
|
|
1819
|
+
until: query.until?.toISOString()
|
|
1820
|
+
} })),
|
|
1821
|
+
commit: verb(true, async (repo, sha, options) => {
|
|
1822
|
+
const [commit, diffs] = await Promise.all([fetcher.json(`${projectPath(repo)}/repository/commits/${sha}`, {
|
|
1823
|
+
query: { stats: "true" },
|
|
1824
|
+
signal: options?.signal
|
|
1825
|
+
}), fetcher.json(`${projectPath(repo)}/repository/commits/${sha}/diff`, { signal: options?.signal })]);
|
|
1826
|
+
return toCommit(repo, commit.data, (diffs.data ?? []).map(toChangedFile));
|
|
1827
|
+
}),
|
|
1828
|
+
compare: verb(true, async (repo, base, head, options) => {
|
|
1829
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/repository/compare`, {
|
|
1830
|
+
query: {
|
|
1831
|
+
from: base,
|
|
1832
|
+
to: head
|
|
1833
|
+
},
|
|
1834
|
+
signal: options?.signal
|
|
1835
|
+
});
|
|
1836
|
+
return {
|
|
1837
|
+
base,
|
|
1838
|
+
head,
|
|
1839
|
+
aheadBy: data.commits?.length,
|
|
1840
|
+
commits: (data.commits ?? []).map((raw) => toCommit(repo, raw)),
|
|
1841
|
+
files: (data.diffs ?? []).map(toChangedFile),
|
|
1842
|
+
raw: data
|
|
1843
|
+
};
|
|
1844
|
+
})
|
|
1845
|
+
},
|
|
1846
|
+
releases: {
|
|
1847
|
+
listPage: verb(true, releasesPage),
|
|
1848
|
+
get: verb(true, async (ref, options) => toRelease(ref.repo, (await fetcher.json(`${projectPath(ref.repo)}/releases/${encodeURIComponent(ref.tag ?? ref.id)}`, { signal: options?.signal })).data)),
|
|
1849
|
+
getByTag: verb(true, async (repo, tag, options) => toRelease(repo, (await fetcher.json(`${projectPath(repo)}/releases/${encodeURIComponent(tag)}`, { signal: options?.signal })).data)),
|
|
1850
|
+
latest: verb(true, async (repo, options) => {
|
|
1851
|
+
const { items } = await releasesPage(repo, {
|
|
1852
|
+
perPage: 20,
|
|
1853
|
+
signal: options?.signal
|
|
1854
|
+
});
|
|
1855
|
+
return items.find((release) => release.publishedAt);
|
|
1856
|
+
}),
|
|
1857
|
+
create: verb(true, async (repo, input, options) => {
|
|
1858
|
+
requirePublishedRelease(input);
|
|
1859
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/releases`, {
|
|
1860
|
+
method: "POST",
|
|
1861
|
+
json: {
|
|
1862
|
+
tag_name: input.tag,
|
|
1863
|
+
ref: input.target,
|
|
1864
|
+
name: input.name,
|
|
1865
|
+
description: input.body
|
|
1866
|
+
},
|
|
1867
|
+
signal: options?.signal
|
|
1868
|
+
});
|
|
1869
|
+
return toRelease(repo, data);
|
|
1870
|
+
}),
|
|
1871
|
+
update: verb(true, async (ref, update, options) => {
|
|
1872
|
+
requirePublishedRelease(update);
|
|
1873
|
+
const tag = ref.tag ?? ref.id;
|
|
1874
|
+
if (update.tag !== void 0 && update.tag !== tag) throw new UnsupportedOperationError("GitLab keys a release by its tag, so the tag cannot change", context);
|
|
1875
|
+
if (update.target !== void 0) throw new UnsupportedOperationError("GitLab cannot change the commit a release points to", context);
|
|
1876
|
+
const { data } = await fetcher.json(`${projectPath(ref.repo)}/releases/${encodeURIComponent(tag)}`, {
|
|
1877
|
+
method: "PUT",
|
|
1878
|
+
json: {
|
|
1879
|
+
name: update.name,
|
|
1880
|
+
description: update.body
|
|
1881
|
+
},
|
|
1882
|
+
signal: options?.signal
|
|
1883
|
+
});
|
|
1884
|
+
return toRelease(ref.repo, data);
|
|
1885
|
+
})
|
|
1886
|
+
},
|
|
1887
|
+
search: {
|
|
1888
|
+
threadsPage: verb(true, searchThreadsPage),
|
|
1889
|
+
reposPage: verb(true, searchReposPage),
|
|
1890
|
+
commitsPage: verb(!anonymous && "experimental", searchCommitsPage),
|
|
1891
|
+
codePage: verb(!anonymous, searchCodePage)
|
|
1892
|
+
},
|
|
1893
|
+
checks: {
|
|
1894
|
+
list: verb(!anonymous, async (repo, sha, options) => ({ items: await Array.fromAsync(fetcher.items(`${projectPath(repo)}/repository/commits/${sha}/statuses`, {
|
|
1895
|
+
query: { per_page: 100 },
|
|
1896
|
+
signal: options?.signal
|
|
1897
|
+
}), (raw) => toStatusCheck(repo, raw)) })),
|
|
1898
|
+
report: verb(true, async (repo, sha, input, options) => toStatusCheck(repo, (await fetcher.json(`${projectPath(repo)}/statuses/${sha}`, {
|
|
1899
|
+
method: "POST",
|
|
1900
|
+
json: {
|
|
1901
|
+
state: STATUS_STATES[input.state],
|
|
1902
|
+
name: input.name,
|
|
1903
|
+
description: input.description,
|
|
1904
|
+
target_url: input.url
|
|
1905
|
+
},
|
|
1906
|
+
signal: options?.signal
|
|
1907
|
+
})).data)),
|
|
1908
|
+
rerun: verb(true, async (ref, options) => {
|
|
1909
|
+
if (ref.type !== "job" && ref.type !== "status") throw new UnsupportedOperationError(`GitLab cannot re-run a ${ref.type}`, context);
|
|
1910
|
+
await fetcher.raw(`${projectPath(ref.repo)}/jobs/${ref.id}/retry`, {
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
signal: options?.signal
|
|
1913
|
+
});
|
|
1914
|
+
})
|
|
1915
|
+
},
|
|
1916
|
+
ci: {
|
|
1917
|
+
runsPage: verb(true, (repo, query = {}) => list(`${projectPath(repo)}/pipelines`, query, (raw) => toPipeline(repo, raw), { query: {
|
|
1918
|
+
ref: query.branch,
|
|
1919
|
+
status: PIPELINE_STATES[query.state ?? "unknown"]
|
|
1920
|
+
} })),
|
|
1921
|
+
run: verb(true, async (ref, options) => toPipeline(ref.repo, (await fetcher.json(`${projectPath(ref.repo)}/pipelines/${ref.id}`, { signal: options?.signal })).data)),
|
|
1922
|
+
jobsPage: verb(true, (ref, listOptions = {}) => list(`${projectPath(ref.repo)}/pipelines/${ref.id}/jobs`, listOptions, (raw) => toCiJob(ref, raw))),
|
|
1923
|
+
log: verb(!anonymous, async (ref, options) => (await fetcher.stream(`${projectPath(ref.repo)}/jobs/${encodeURIComponent(ref.id)}/trace`, { signal: options?.signal })).body)
|
|
1924
|
+
},
|
|
1925
|
+
securityAlerts: {
|
|
1926
|
+
kinds: {
|
|
1927
|
+
dependency: !anonymous,
|
|
1928
|
+
code_scanning: !anonymous,
|
|
1929
|
+
secret: !anonymous
|
|
1930
|
+
},
|
|
1931
|
+
listPage: vulnerabilitiesPage
|
|
1932
|
+
},
|
|
1933
|
+
threads: {
|
|
1934
|
+
filesPage: verb(true, (thread, listOptions = {}) => list(`${threadPath(requireMerge(thread, "read for changed files"))}/diffs`, listOptions, toChangedFile)),
|
|
1935
|
+
commitsPage: verb(true, (thread, listOptions = {}) => list(`${threadPath(requireMerge(thread, "read for commits"))}/commits`, listOptions, (raw) => toCommit(thread.repo, raw))),
|
|
1936
|
+
reviewsPage: verb("emulated", async (thread, listOptions) => {
|
|
1937
|
+
const ref = requireThread(thread, context);
|
|
1938
|
+
if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests have approvals", context);
|
|
1939
|
+
const { data } = await fetcher.json(`${threadPath(ref)}/approvals`, { signal: listOptions?.signal });
|
|
1940
|
+
return { items: toApprovalReviews(ref, data) };
|
|
1941
|
+
}),
|
|
1942
|
+
approve: verb(true, async (thread, body, options) => {
|
|
1943
|
+
await approve(thread, body, options);
|
|
1944
|
+
}),
|
|
1945
|
+
createReview: verb("emulated", async (thread, input, options) => {
|
|
1946
|
+
if (input.comments?.length) throw new UnsupportedOperationError("GitLab reviews carry no inline comments yet", context);
|
|
1947
|
+
const ref = requireThread(thread, context);
|
|
1948
|
+
if (input.event === "comment" && input.body) {
|
|
1949
|
+
const { data } = await fetcher.json(`${threadPath(ref)}/discussions`, {
|
|
1950
|
+
method: "POST",
|
|
1951
|
+
json: { body: input.body },
|
|
1952
|
+
signal: options?.signal
|
|
1953
|
+
});
|
|
1954
|
+
const note = data.notes[0];
|
|
1955
|
+
return {
|
|
1956
|
+
...syntheticReview(ref, `discussion:${data.id}`, "commented", {
|
|
1957
|
+
author: toActor(instance, note.author),
|
|
1958
|
+
stateRaw: "discussion",
|
|
1959
|
+
submittedAt: toDate(note.created_at),
|
|
1960
|
+
raw: data
|
|
1961
|
+
}),
|
|
1962
|
+
body: note.body,
|
|
1963
|
+
comments: [{
|
|
1964
|
+
...toNoteComment(ref, note, data),
|
|
1965
|
+
raw: note
|
|
1966
|
+
}]
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
if (input.event !== "approve") throw new UnsupportedOperationError("GitLab has approvals and discussions, not reviews; create `approve`, or `comment` with a body", context);
|
|
1970
|
+
const approvals = await approve(ref, input.body, options);
|
|
1971
|
+
const viewer = await viewerLogin();
|
|
1972
|
+
const reviews = toApprovalReviews(ref, approvals);
|
|
1973
|
+
return reviews.find((review) => review.author?.login === viewer) ?? reviews.at(-1) ?? {
|
|
1974
|
+
ref: {
|
|
1975
|
+
forge: "gitlab",
|
|
1976
|
+
instance,
|
|
1977
|
+
thread: ref,
|
|
1978
|
+
id: "approval:"
|
|
1979
|
+
},
|
|
1980
|
+
state: "approved",
|
|
1981
|
+
stateRaw: "approved",
|
|
1982
|
+
comments: false,
|
|
1983
|
+
raw: approvals
|
|
1984
|
+
};
|
|
1985
|
+
}),
|
|
1986
|
+
reviewThreads: verb(true, {
|
|
1987
|
+
resolveReviewThread: (thread, id, options) => setDiscussionResolved(thread, id, true, options),
|
|
1988
|
+
unresolveReviewThread: (thread, id, options) => setDiscussionResolved(thread, id, false, options)
|
|
1989
|
+
}),
|
|
1990
|
+
checks: perKind({ pull_request: true }, async (thread, options) => {
|
|
1991
|
+
const ref = requireThread(thread, context);
|
|
1992
|
+
const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
|
|
1993
|
+
if (!data.head_pipeline) return { items: [] };
|
|
1994
|
+
const pipelineProject = data.head_pipeline.project_id;
|
|
1995
|
+
const project = pipelineProject === void 0 || pipelineProject === data.target_project_id ? projectPath(ref.repo) : `/projects/${pipelineProject}`;
|
|
1996
|
+
return { items: await Array.fromAsync(fetcher.items(`${project}/pipelines/${data.head_pipeline.id}/jobs`, {
|
|
1997
|
+
query: { per_page: 100 },
|
|
1998
|
+
signal: options?.signal
|
|
1999
|
+
}), (job) => toJobCheck(ref.repo, job)) };
|
|
2000
|
+
}),
|
|
2001
|
+
get: perKind({
|
|
2002
|
+
issue: true,
|
|
2003
|
+
pull_request: true,
|
|
2004
|
+
commit: true
|
|
2005
|
+
}, get),
|
|
2006
|
+
getMany: verb(true, (refs, options) => getManyConcurrently(refs, get, options)),
|
|
2007
|
+
listPage: perKind(ISSUE_LIKE, listPage),
|
|
2008
|
+
eventsPage: verb(!anonymous, async (thread, listOptions = {}) => {
|
|
2009
|
+
const ref = requireThread(thread, context);
|
|
2010
|
+
if (ref.kind === "commit") {
|
|
2011
|
+
const offset = Number(listOptions.cursor?.token ?? 0);
|
|
2012
|
+
const result = await fetcher.page(`${threadPath(ref)}/comments`, {
|
|
2013
|
+
query: { per_page: listOptions.perPage },
|
|
2014
|
+
cursor: listOptions.cursor,
|
|
2015
|
+
signal: listOptions.signal
|
|
2016
|
+
});
|
|
2017
|
+
let index = offset;
|
|
2018
|
+
const page = toPage(result, (comment) => toCommitCommentEvent(ref, comment, index++));
|
|
2019
|
+
return page.cursor ? {
|
|
2020
|
+
...page,
|
|
2021
|
+
cursor: {
|
|
2022
|
+
...page.cursor,
|
|
2023
|
+
token: String(index)
|
|
2024
|
+
}
|
|
2025
|
+
} : page;
|
|
2026
|
+
}
|
|
2027
|
+
return list(`${threadPath(ref)}/notes`, listOptions, (note) => toNoteEvent(ref, note), { query: {
|
|
2028
|
+
sort: "asc",
|
|
2029
|
+
order_by: "created_at"
|
|
2030
|
+
} });
|
|
2031
|
+
}),
|
|
2032
|
+
commentsPage: perKind({
|
|
2033
|
+
issue: !anonymous,
|
|
2034
|
+
pull_request: !anonymous,
|
|
2035
|
+
commit: true
|
|
2036
|
+
}, async (thread, listOptions = {}) => {
|
|
2037
|
+
const ref = requireThread(thread, context);
|
|
2038
|
+
if (ref.kind === "commit") return list(`${threadPath(ref)}/comments`, listOptions, (comment) => toCommitComment(ref, comment));
|
|
2039
|
+
const page = await list(`${threadPath(requireIssueOrPull(ref, context, "list comments on"))}/discussions`, listOptions, (discussion) => discussion);
|
|
2040
|
+
return {
|
|
2041
|
+
...page,
|
|
2042
|
+
items: page.items.flatMap((discussion) => discussion.notes.filter((note) => !note.system).map((note) => toNoteComment(ref, note, discussion)))
|
|
2043
|
+
};
|
|
2044
|
+
}),
|
|
2045
|
+
comment: perKind({
|
|
2046
|
+
issue: true,
|
|
2047
|
+
pull_request: true,
|
|
2048
|
+
commit: true
|
|
2049
|
+
}, async (thread, body, options) => {
|
|
2050
|
+
const ref = requireThread(thread, context);
|
|
2051
|
+
if (ref.kind === "commit") {
|
|
2052
|
+
const { data } = await fetcher.json(`${threadPath(ref)}/comments`, {
|
|
2053
|
+
method: "POST",
|
|
2054
|
+
json: { note: body },
|
|
2055
|
+
signal: options?.signal
|
|
2056
|
+
});
|
|
2057
|
+
return toCommitComment(ref, data);
|
|
2058
|
+
}
|
|
2059
|
+
const { data } = await fetcher.json(`${threadPath(requireIssueOrPull(ref, context, "comment on"))}/notes`, {
|
|
2060
|
+
method: "POST",
|
|
2061
|
+
json: { body },
|
|
2062
|
+
signal: options?.signal
|
|
2063
|
+
});
|
|
2064
|
+
return toNoteComment(ref, data);
|
|
2065
|
+
}),
|
|
2066
|
+
editComment: perKind({
|
|
2067
|
+
issue: true,
|
|
2068
|
+
pull_request: true
|
|
2069
|
+
}, async (ref, body, options) => {
|
|
2070
|
+
const thread = requireIssueOrPull(ref.thread, context, "edit comments on");
|
|
2071
|
+
const { data } = await fetcher.json(`${threadPath(thread)}/notes/${ref.id}`, {
|
|
2072
|
+
method: "PUT",
|
|
2073
|
+
json: { body },
|
|
2074
|
+
signal: options?.signal
|
|
2075
|
+
});
|
|
2076
|
+
return toNoteComment(thread, data);
|
|
2077
|
+
}),
|
|
2078
|
+
deleteComment: perKind({
|
|
2079
|
+
issue: true,
|
|
2080
|
+
pull_request: true
|
|
2081
|
+
}, async (ref, options) => {
|
|
2082
|
+
const thread = requireIssueOrPull(ref.thread, context, "delete comments on");
|
|
2083
|
+
await fetcher.raw(`${threadPath(thread)}/notes/${ref.id}`, {
|
|
2084
|
+
method: "DELETE",
|
|
2085
|
+
signal: options?.signal
|
|
2086
|
+
});
|
|
2087
|
+
}),
|
|
2088
|
+
create: perKind({
|
|
2089
|
+
issue: true,
|
|
2090
|
+
pull_request: true
|
|
2091
|
+
}, async (repo, input, options) => {
|
|
2092
|
+
if (input.kind === "discussion") throw new UnsupportedOperationError("GitLab has no discussion threads", context);
|
|
2093
|
+
const assignee_ids = input.assignees?.length ? await Promise.all(input.assignees.map(userId)) : void 0;
|
|
2094
|
+
const labels = input.labels?.join(",");
|
|
2095
|
+
if (input.kind === "issue") {
|
|
2096
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/issues`, {
|
|
2097
|
+
method: "POST",
|
|
2098
|
+
json: {
|
|
2099
|
+
title: input.title,
|
|
2100
|
+
description: input.body,
|
|
2101
|
+
labels,
|
|
2102
|
+
assignee_ids
|
|
2103
|
+
},
|
|
2104
|
+
signal: options?.signal
|
|
2105
|
+
});
|
|
2106
|
+
return toThread({
|
|
2107
|
+
forge: FORGE,
|
|
2108
|
+
instance,
|
|
2109
|
+
repo,
|
|
2110
|
+
kind: "issue",
|
|
2111
|
+
number: String(data.iid)
|
|
2112
|
+
}, data);
|
|
2113
|
+
}
|
|
2114
|
+
if (!input.head || !input.base) throw new UnsupportedOperationError("Creating a merge request needs head and base branches", context);
|
|
2115
|
+
const { data } = await fetcher.json(`${projectPath(repo)}/merge_requests`, {
|
|
2116
|
+
method: "POST",
|
|
2117
|
+
json: {
|
|
2118
|
+
title: input.draft ? `Draft: ${input.title}` : input.title,
|
|
2119
|
+
description: input.body,
|
|
2120
|
+
source_branch: input.head,
|
|
2121
|
+
target_branch: input.base,
|
|
2122
|
+
labels,
|
|
2123
|
+
assignee_ids
|
|
2124
|
+
},
|
|
2125
|
+
signal: options?.signal
|
|
2126
|
+
});
|
|
2127
|
+
return toThread({
|
|
2128
|
+
forge: FORGE,
|
|
2129
|
+
instance,
|
|
2130
|
+
repo,
|
|
2131
|
+
kind: "pull_request",
|
|
2132
|
+
number: String(data.iid)
|
|
2133
|
+
}, data);
|
|
2134
|
+
}),
|
|
2135
|
+
update: perKind({
|
|
2136
|
+
issue: true,
|
|
2137
|
+
pull_request: true
|
|
2138
|
+
}, async (thread, input, options) => {
|
|
2139
|
+
const ref = requireIssueOrPull(thread, context, "update");
|
|
2140
|
+
const { data } = await fetcher.json(threadPath(ref), {
|
|
2141
|
+
method: "PUT",
|
|
2142
|
+
json: {
|
|
2143
|
+
title: input.title,
|
|
2144
|
+
description: input.body
|
|
2145
|
+
},
|
|
2146
|
+
signal: options?.signal
|
|
2147
|
+
});
|
|
2148
|
+
return toThread(ref, data);
|
|
2149
|
+
}),
|
|
2150
|
+
addLabels: perKind({
|
|
2151
|
+
issue: true,
|
|
2152
|
+
pull_request: true
|
|
2153
|
+
}, async (thread, labels, options) => {
|
|
2154
|
+
const ref = requireIssueOrPull(thread, context, "label");
|
|
2155
|
+
await fetcher.raw(threadPath(ref), {
|
|
2156
|
+
method: "PUT",
|
|
2157
|
+
json: { add_labels: labels.join(",") },
|
|
2158
|
+
signal: options?.signal
|
|
2159
|
+
});
|
|
2160
|
+
}),
|
|
2161
|
+
removeLabels: perKind({
|
|
2162
|
+
issue: true,
|
|
2163
|
+
pull_request: true
|
|
2164
|
+
}, async (thread, labels, options) => {
|
|
2165
|
+
const ref = requireIssueOrPull(thread, context, "label");
|
|
2166
|
+
await fetcher.raw(threadPath(ref), {
|
|
2167
|
+
method: "PUT",
|
|
2168
|
+
json: { remove_labels: labels.join(",") },
|
|
2169
|
+
signal: options?.signal
|
|
2170
|
+
});
|
|
2171
|
+
}),
|
|
2172
|
+
setMilestone: perKind({
|
|
2173
|
+
issue: true,
|
|
2174
|
+
pull_request: true
|
|
2175
|
+
}, async (thread, milestone, options) => {
|
|
2176
|
+
const ref = requireIssueOrPull(thread, context, "set the milestone of");
|
|
2177
|
+
await fetcher.raw(threadPath(ref), {
|
|
2178
|
+
method: "PUT",
|
|
2179
|
+
json: { milestone_id: milestone === void 0 ? 0 : await milestoneId(milestone, (page) => milestonesPage(ref.repo, page, true), context, options) },
|
|
2180
|
+
signal: options?.signal
|
|
2181
|
+
});
|
|
2182
|
+
}),
|
|
2183
|
+
reactionsPage: perKind(ISSUE_LIKE, async (target, listOptions = {}) => {
|
|
2184
|
+
const result = await fetcher.page(`${awardPath(target)}/award_emoji`, {
|
|
2185
|
+
query: { per_page: listOptions.perPage },
|
|
2186
|
+
cursor: listOptions.cursor,
|
|
2187
|
+
signal: listOptions.signal
|
|
2188
|
+
});
|
|
2189
|
+
return toPage(result, (raw) => {
|
|
2190
|
+
const reactor = toActor(instance, raw.user);
|
|
2191
|
+
return reactor && {
|
|
2192
|
+
content: reactionContent(REACTION_NAMES[raw.name] ?? raw.name),
|
|
2193
|
+
contentRaw: raw.name,
|
|
2194
|
+
actor: reactor,
|
|
2195
|
+
createdAt: toDate(raw.created_at),
|
|
2196
|
+
raw
|
|
2197
|
+
};
|
|
2198
|
+
});
|
|
2199
|
+
}),
|
|
2200
|
+
reactions: perKind({
|
|
2201
|
+
issue: true,
|
|
2202
|
+
pull_request: true
|
|
2203
|
+
}, {
|
|
2204
|
+
async react(target, reaction, options) {
|
|
2205
|
+
await fetcher.raw(`${awardPath(target)}/award_emoji`, {
|
|
2206
|
+
method: "POST",
|
|
2207
|
+
json: { name: AWARD_EMOJI[reaction] },
|
|
2208
|
+
signal: options?.signal
|
|
2209
|
+
});
|
|
2210
|
+
},
|
|
2211
|
+
async unreact(target, reaction, options) {
|
|
2212
|
+
const path = `${awardPath(target)}/award_emoji`;
|
|
2213
|
+
const { data } = await fetcher.json(path, {
|
|
2214
|
+
query: { per_page: 100 },
|
|
2215
|
+
signal: options?.signal
|
|
2216
|
+
});
|
|
2217
|
+
const login = await viewerLogin();
|
|
2218
|
+
const mine = data.find((award) => award.name === AWARD_EMOJI[reaction] && award.user?.username === login);
|
|
2219
|
+
if (mine) await fetcher.raw(`${path}/${mine.id}`, {
|
|
2220
|
+
method: "DELETE",
|
|
2221
|
+
signal: options?.signal
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}),
|
|
2225
|
+
transfer: verb(true, async (thread, repo, options) => {
|
|
2226
|
+
const ref = requireIssueOrPull(thread, context, "transfer");
|
|
2227
|
+
if (ref.kind !== "issue") throw new UnsupportedOperationError("GitLab moves issues only", context);
|
|
2228
|
+
const { data: target } = await fetcher.json(`/projects/${projectId(repo)}`, { signal: options?.signal });
|
|
2229
|
+
const { data } = await fetcher.json(`${threadPath(ref)}/move`, {
|
|
2230
|
+
method: "POST",
|
|
2231
|
+
json: { to_project_id: target.id },
|
|
2232
|
+
signal: options?.signal
|
|
2233
|
+
});
|
|
2234
|
+
return {
|
|
2235
|
+
forge: FORGE,
|
|
2236
|
+
instance,
|
|
2237
|
+
repo,
|
|
2238
|
+
kind: "issue",
|
|
2239
|
+
number: String(data.iid),
|
|
2240
|
+
externalId: data.id === void 0 ? void 0 : String(data.id)
|
|
2241
|
+
};
|
|
2242
|
+
}),
|
|
2243
|
+
markDuplicate: verb("emulated", async (thread, canonical, options) => {
|
|
2244
|
+
const ref = requireIssueOrPull(thread, context, "mark duplicate");
|
|
2245
|
+
const reference = canonical.repo.owner === ref.repo.owner && canonical.repo.name === ref.repo.name ? `#${canonical.number}` : `${canonical.repo.owner}/${canonical.repo.name}#${canonical.number}`;
|
|
2246
|
+
await fetcher.raw(`${threadPath(ref)}/notes`, {
|
|
2247
|
+
method: "POST",
|
|
2248
|
+
json: { body: `/duplicate ${reference}` },
|
|
2249
|
+
signal: options?.signal
|
|
2250
|
+
});
|
|
2251
|
+
}),
|
|
2252
|
+
setLabels: perKind({
|
|
2253
|
+
issue: true,
|
|
2254
|
+
pull_request: true
|
|
2255
|
+
}, async (thread, labels, options) => {
|
|
2256
|
+
const ref = requireIssueOrPull(thread, context, "label");
|
|
2257
|
+
await fetcher.raw(threadPath(ref), {
|
|
2258
|
+
method: "PUT",
|
|
2259
|
+
json: { labels: labels.join(",") },
|
|
2260
|
+
signal: options?.signal
|
|
2261
|
+
});
|
|
2262
|
+
}),
|
|
2263
|
+
setAssignees: perKind({
|
|
2264
|
+
issue: true,
|
|
2265
|
+
pull_request: true
|
|
2266
|
+
}, async (thread, assignees, options) => {
|
|
2267
|
+
const ref = requireIssueOrPull(thread, context, "assign");
|
|
2268
|
+
await fetcher.raw(threadPath(ref), {
|
|
2269
|
+
method: "PUT",
|
|
2270
|
+
json: { assignee_ids: await Promise.all(assignees.map(userId)) },
|
|
2271
|
+
signal: options?.signal
|
|
2272
|
+
});
|
|
2273
|
+
}),
|
|
2274
|
+
requestReview: perKind({ pull_request: true }, async (thread, reviewers, options) => {
|
|
2275
|
+
const ref = requireThread(thread, context);
|
|
2276
|
+
if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests have reviewers", context);
|
|
2277
|
+
const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
|
|
2278
|
+
const ids = /* @__PURE__ */ new Set([...(data.reviewers ?? []).map((user) => user.id), ...await Promise.all(reviewers.map(userId))]);
|
|
2279
|
+
await fetcher.raw(threadPath(ref), {
|
|
2280
|
+
method: "PUT",
|
|
2281
|
+
json: { reviewer_ids: [...ids] },
|
|
2282
|
+
signal: options?.signal
|
|
2283
|
+
});
|
|
2284
|
+
}),
|
|
2285
|
+
close: perKind({
|
|
2286
|
+
issue: true,
|
|
2287
|
+
pull_request: true
|
|
2288
|
+
}, (ref, options) => setState(ref, "close", options)),
|
|
2289
|
+
reopen: perKind({
|
|
2290
|
+
issue: true,
|
|
2291
|
+
pull_request: true
|
|
2292
|
+
}, (ref, options) => setState(ref, "reopen", options)),
|
|
2293
|
+
merge: verb(true, merge),
|
|
2294
|
+
subscriptions: perKind(ISSUE_LIKE, {
|
|
2295
|
+
subscription: async (thread, options) => {
|
|
2296
|
+
const ref = requireIssueOrPull(thread, context, "read the subscription of");
|
|
2297
|
+
return (await fetcher.json(threadPath(ref), { signal: options?.signal })).data.subscribed ? "subscribed" : "none";
|
|
2298
|
+
},
|
|
2299
|
+
subscribe: (thread, options) => setSubscribed(thread, true, options),
|
|
2300
|
+
unsubscribe: (thread, options) => setSubscribed(thread, false, options)
|
|
2301
|
+
})
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
/** Creates a GitLab provider for gitlab.com or a self-managed instance. */
|
|
2307
|
+
const gitlab = /* @__PURE__ */ defineForgeProvider({
|
|
2308
|
+
...GITLAB,
|
|
2309
|
+
webhooks: gitlabWebhooks
|
|
2310
|
+
});
|
|
2311
|
+
/** `gitlab()` without webhook ingestion, for bundles that never receive a delivery. */
|
|
2312
|
+
const gitlabLite = /* @__PURE__ */ defineForgeProvider(GITLAB);
|
|
2313
|
+
/** GitLab tokens carry coarse scopes; `api` covers writes and `read_api` reads. */
|
|
2314
|
+
function gitlabScopesFor(verb) {
|
|
2315
|
+
const [group = "", name = ""] = verb.split(".");
|
|
2316
|
+
if (group === "webhooks") return name === "verify" || name === "ingest" ? {} : {
|
|
2317
|
+
token: ["api"],
|
|
2318
|
+
note: "Maintainer on the project, or Owner on the group"
|
|
2319
|
+
};
|
|
2320
|
+
return { token: [(/* @__PURE__ */ new Set([
|
|
2321
|
+
"get",
|
|
2322
|
+
"list",
|
|
2323
|
+
"listPage",
|
|
2324
|
+
"page",
|
|
2325
|
+
"events",
|
|
2326
|
+
"eventsPage",
|
|
2327
|
+
"comments",
|
|
2328
|
+
"commentsPage",
|
|
2329
|
+
"getMany",
|
|
2330
|
+
"reviews",
|
|
2331
|
+
"reviewsPage",
|
|
2332
|
+
"checks",
|
|
2333
|
+
"files",
|
|
2334
|
+
"filesPage",
|
|
2335
|
+
"commits",
|
|
2336
|
+
"commitsPage",
|
|
2337
|
+
"code",
|
|
2338
|
+
"codePage",
|
|
2339
|
+
"subscription",
|
|
2340
|
+
"unreadCount",
|
|
2341
|
+
"threads",
|
|
2342
|
+
"threadsPage",
|
|
2343
|
+
"repos",
|
|
2344
|
+
"reposPage",
|
|
2345
|
+
"tree",
|
|
2346
|
+
"treePage",
|
|
2347
|
+
"branches",
|
|
2348
|
+
"branchesPage",
|
|
2349
|
+
"tags",
|
|
2350
|
+
"tagsPage",
|
|
2351
|
+
"file",
|
|
2352
|
+
"compare",
|
|
2353
|
+
"resolveRef",
|
|
2354
|
+
"commit",
|
|
2355
|
+
"latest",
|
|
2356
|
+
"getByTag",
|
|
2357
|
+
"downloadAsset",
|
|
2358
|
+
"runs",
|
|
2359
|
+
"runsPage",
|
|
2360
|
+
"run",
|
|
2361
|
+
"jobs",
|
|
2362
|
+
"jobsPage",
|
|
2363
|
+
"log",
|
|
2364
|
+
"permissionFor",
|
|
2365
|
+
"labels",
|
|
2366
|
+
"labelsPage",
|
|
2367
|
+
"milestones",
|
|
2368
|
+
"milestonesPage",
|
|
2369
|
+
"collaborators",
|
|
2370
|
+
"collaboratorsPage",
|
|
2371
|
+
"assignableUsers",
|
|
2372
|
+
"assignableUsersPage",
|
|
2373
|
+
"reviewerCandidates",
|
|
2374
|
+
"reviewerCandidatesPage"
|
|
2375
|
+
])).has(name) ? "read_api" : "api"] };
|
|
2376
|
+
}
|
|
2377
|
+
//#endregion
|
|
2378
|
+
export { gitlabLite as n, gitlabScopesFor as r, gitlab as t };
|