@ycodium-ai/plugin-pull-requests 0.2.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/README.md +62 -0
- package/assets/ui.js +1830 -0
- package/dist/index.js +4688 -0
- package/locales/en.json +140 -0
- package/locales/zh-CN.json +140 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4688 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/service/backoff.ts
|
|
5
|
+
var FALLBACK_COOLDOWN_MS = 3e4;
|
|
6
|
+
var MAX_FALLBACK_COOLDOWN_MS = 15 * 6e4;
|
|
7
|
+
var fallbackCooldownMs = (attempt) => Math.min(FALLBACK_COOLDOWN_MS * 2 ** Math.max(0, attempt - 1), MAX_FALLBACK_COOLDOWN_MS);
|
|
8
|
+
var createBackoff = (options) => {
|
|
9
|
+
const now = options?.now ?? Date.now;
|
|
10
|
+
const entries = /* @__PURE__ */ new Map();
|
|
11
|
+
const keyOf = (input) => `${input.provider}\0${input.host.trim().toLowerCase()}`;
|
|
12
|
+
return {
|
|
13
|
+
check: (key, checkOptions) => {
|
|
14
|
+
const entry = entries.get(keyOf(key));
|
|
15
|
+
const at2 = now();
|
|
16
|
+
if (entry !== void 0 && entry.retryAt > at2 && checkOptions?.allowPaused !== true) {
|
|
17
|
+
return {
|
|
18
|
+
provider: key.provider,
|
|
19
|
+
host: key.host.trim().toLowerCase(),
|
|
20
|
+
retryAt: entry.retryAt
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { lease: entry?.generation ?? 0 };
|
|
24
|
+
},
|
|
25
|
+
recordRateLimit: (input) => {
|
|
26
|
+
const key = keyOf(input);
|
|
27
|
+
const at2 = now();
|
|
28
|
+
const previous = entries.get(key);
|
|
29
|
+
if (previous !== void 0 && previous.generation > input.lease) {
|
|
30
|
+
if (previous.retryAt <= at2 && (input.retryAt === void 0 || input.retryAt <= at2)) return;
|
|
31
|
+
const retryAt = input.retryAt !== void 0 && input.retryAt > previous.retryAt ? input.retryAt : previous.retryAt;
|
|
32
|
+
if (retryAt === previous.retryAt) return;
|
|
33
|
+
entries.set(key, { ...previous, retryAt });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const attempt = (previous?.attempt ?? 0) + 1;
|
|
37
|
+
const proposed = input.retryAt !== void 0 && input.retryAt > at2 ? input.retryAt : at2 + fallbackCooldownMs(attempt);
|
|
38
|
+
entries.set(key, {
|
|
39
|
+
attempt,
|
|
40
|
+
generation: Math.max(previous?.generation ?? 0, input.lease) + 1,
|
|
41
|
+
retryAt: previous !== void 0 && previous.retryAt > at2 ? Math.max(previous.retryAt, proposed) : proposed
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
recordSuccess: (input) => {
|
|
45
|
+
const key = keyOf(input);
|
|
46
|
+
const previous = entries.get(key);
|
|
47
|
+
if (previous === void 0 || previous.generation !== input.lease || previous.retryAt > now()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
entries.set(key, { attempt: 0, generation: previous.generation, retryAt: 0 });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
var isPaused = (result) => "retryAt" in result;
|
|
55
|
+
|
|
56
|
+
// src/service/cache.ts
|
|
57
|
+
var createCache = (options) => {
|
|
58
|
+
const now = options.now ?? Date.now;
|
|
59
|
+
const held = /* @__PURE__ */ new Map();
|
|
60
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
61
|
+
const record = (key, value) => {
|
|
62
|
+
held.delete(key);
|
|
63
|
+
if (held.size >= options.capacity) {
|
|
64
|
+
const oldest = held.keys().next().value;
|
|
65
|
+
if (oldest !== void 0) held.delete(oldest);
|
|
66
|
+
}
|
|
67
|
+
held.set(key, { at: now(), value });
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
get: (key, load) => {
|
|
71
|
+
const snapshot = held.get(key);
|
|
72
|
+
if (snapshot !== void 0 && now() - snapshot.at <= options.ttlMs(key)) {
|
|
73
|
+
return Promise.resolve(snapshot.value);
|
|
74
|
+
}
|
|
75
|
+
if (snapshot !== void 0) held.delete(key);
|
|
76
|
+
const running = inFlight.get(key);
|
|
77
|
+
if (running !== void 0) return running;
|
|
78
|
+
const started = load().then(
|
|
79
|
+
(value) => {
|
|
80
|
+
inFlight.delete(key);
|
|
81
|
+
record(key, value);
|
|
82
|
+
return value;
|
|
83
|
+
},
|
|
84
|
+
(error) => {
|
|
85
|
+
inFlight.delete(key);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
inFlight.set(key, started);
|
|
90
|
+
return started;
|
|
91
|
+
},
|
|
92
|
+
clear: () => {
|
|
93
|
+
held.clear();
|
|
94
|
+
},
|
|
95
|
+
size: () => held.size
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
var createStaleWindow = (options) => {
|
|
99
|
+
const now = options.now ?? Date.now;
|
|
100
|
+
const held = /* @__PURE__ */ new Map();
|
|
101
|
+
const record = (key, value) => {
|
|
102
|
+
held.delete(key);
|
|
103
|
+
if (held.size >= options.capacity) {
|
|
104
|
+
const oldest = held.keys().next().value;
|
|
105
|
+
if (oldest !== void 0) held.delete(oldest);
|
|
106
|
+
}
|
|
107
|
+
held.set(key, { at: now(), value });
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
get: (key, read) => {
|
|
111
|
+
const recorded = () => read().then((value) => {
|
|
112
|
+
record(key, value);
|
|
113
|
+
return value;
|
|
114
|
+
});
|
|
115
|
+
const snapshot = held.get(key);
|
|
116
|
+
if (snapshot === void 0 || now() - snapshot.at > options.staleMs) return recorded();
|
|
117
|
+
void recorded().catch((error) => options.onRefreshFailure?.(error));
|
|
118
|
+
return Promise.resolve(snapshot.value);
|
|
119
|
+
},
|
|
120
|
+
clear: () => {
|
|
121
|
+
held.clear();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/service/cursor.ts
|
|
127
|
+
var LIST_CURSOR_PATTERN = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))\|(\d{1,9})\|(\d{1,9}(?:,\d{1,9})*)?$/;
|
|
128
|
+
var parseListCursor = (raw) => {
|
|
129
|
+
const match = LIST_CURSOR_PATTERN.exec(raw);
|
|
130
|
+
if (match === null) return null;
|
|
131
|
+
const seenAt = match[3];
|
|
132
|
+
return {
|
|
133
|
+
updatedBefore: match[1],
|
|
134
|
+
delivered: Number(match[2]),
|
|
135
|
+
seenAt: seenAt === void 0 ? [] : seenAt.split(",").map(Number)
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
var listCursorKey = (host, repository) => `${host} ${repository.toLowerCase()}`;
|
|
139
|
+
var listCursorAt = (previous, boundary, fetched, deliveredCount) => {
|
|
140
|
+
const seenAt = [
|
|
141
|
+
...previous?.updatedBefore === boundary ? previous.seenAt : [],
|
|
142
|
+
...fetched.filter((item) => item.updatedAt === boundary).map((item) => item.number)
|
|
143
|
+
];
|
|
144
|
+
return `${boundary}|${(previous?.delivered ?? 0) + deliveredCount}|${seenAt.join(",")}`;
|
|
145
|
+
};
|
|
146
|
+
var nextListCursor = (previous, fetched, delivered, cursorAdvance = delivered.length) => {
|
|
147
|
+
if (fetched.length === 0) return null;
|
|
148
|
+
const oldest = fetched.reduce((left, right) => right.updatedAt < left.updatedAt ? right : left);
|
|
149
|
+
return listCursorAt(previous, oldest.updatedAt, fetched, cursorAdvance);
|
|
150
|
+
};
|
|
151
|
+
var withoutAlreadySent = (cursor, items) => cursor === void 0 ? items : items.filter(
|
|
152
|
+
(item) => item.updatedAt !== cursor.updatedBefore || !cursor.seenAt.includes(item.number)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// src/service/errors.ts
|
|
156
|
+
var PROVIDER_REQUIREMENT = {
|
|
157
|
+
github: {
|
|
158
|
+
tokenMissing: "Add a GitHub token to the Pull requests card under Settings \u2192 Plugins to browse pull requests on this host.",
|
|
159
|
+
tokenRejected: "GitHub rejected the configured token. Replace it in the Pull requests card under Settings \u2192 Plugins."
|
|
160
|
+
},
|
|
161
|
+
gitlab: {
|
|
162
|
+
tokenMissing: "Add a GitLab token to the Pull requests card under Settings \u2192 Plugins to browse merge requests on this host.",
|
|
163
|
+
tokenRejected: "GitLab rejected the configured token. Replace it in the Pull requests card under Settings \u2192 Plugins."
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var unavailableMessage = (reason, provider) => {
|
|
167
|
+
const requirement = provider === void 0 ? void 0 : PROVIDER_REQUIREMENT[provider];
|
|
168
|
+
switch (reason) {
|
|
169
|
+
case "token-missing":
|
|
170
|
+
return requirement?.tokenMissing ?? "No token is configured for this host.";
|
|
171
|
+
case "token-rejected":
|
|
172
|
+
return requirement?.tokenRejected ?? "This host rejected the configured token.";
|
|
173
|
+
case "provider-unsupported":
|
|
174
|
+
return "Change requests cannot be browsed for this project's host yet.";
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var providerRequirement = (provider, reason) => {
|
|
178
|
+
const requirement = PROVIDER_REQUIREMENT[provider];
|
|
179
|
+
if (requirement === void 0) return null;
|
|
180
|
+
switch (reason) {
|
|
181
|
+
case "token-missing":
|
|
182
|
+
return requirement.tokenMissing;
|
|
183
|
+
case "token-rejected":
|
|
184
|
+
return requirement.tokenRejected;
|
|
185
|
+
case "provider-unsupported":
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var PullRequestUnavailable = class extends Error {
|
|
190
|
+
code;
|
|
191
|
+
data;
|
|
192
|
+
constructor(input) {
|
|
193
|
+
super(unavailableMessage(input.reason, input.provider));
|
|
194
|
+
this.name = "PullRequestUnavailable";
|
|
195
|
+
this.code = input.reason;
|
|
196
|
+
this.data = {
|
|
197
|
+
...input.provider === void 0 ? {} : { provider: input.provider },
|
|
198
|
+
...input.host === void 0 ? {} : { host: input.host }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var PullRequestOperationFailed = class extends Error {
|
|
203
|
+
code = "operation-failed";
|
|
204
|
+
data;
|
|
205
|
+
constructor(input) {
|
|
206
|
+
super(`Pull request operation ${input.operation} failed: ${input.detail}`);
|
|
207
|
+
this.name = "PullRequestOperationFailed";
|
|
208
|
+
this.data = {
|
|
209
|
+
operation: input.operation,
|
|
210
|
+
detail: input.detail,
|
|
211
|
+
...input.retryAt === void 0 ? {} : { retryAt: input.retryAt }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/githubHttp.ts
|
|
217
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
218
|
+
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
219
|
+
var DEFAULT_HOST = "github.com";
|
|
220
|
+
var RESERVE_RATIO = 0.1;
|
|
221
|
+
var CARD = "Settings \u2192 Plugins \u2192 Pull requests";
|
|
222
|
+
var GitHubError = class extends Error {
|
|
223
|
+
code;
|
|
224
|
+
/** 只有限流会带:`{ retryAt: epoch ms }`。宿主原样透传给 port。 */
|
|
225
|
+
data;
|
|
226
|
+
constructor(input) {
|
|
227
|
+
super(`GitHub API failed in ${input.operation}: ${input.detail}`);
|
|
228
|
+
this.name = "GitHubError";
|
|
229
|
+
this.code = input.code;
|
|
230
|
+
if (input.data !== void 0) this.data = input.data;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var text = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
234
|
+
var normalizeHost = (value) => value.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "").toLowerCase();
|
|
235
|
+
var configuredHost = (ctx) => normalizeHost(text(ctx.config["githubHost"]) ?? DEFAULT_HOST);
|
|
236
|
+
var configuredToken = (ctx) => text(ctx.config["githubToken"]);
|
|
237
|
+
var restBase = (host) => host === DEFAULT_HOST ? "https://api.github.com" : `https://${host}/api/v3`;
|
|
238
|
+
var graphQlUrl = (host) => host === DEFAULT_HOST ? "https://api.github.com/graphql" : `https://${host}/api/graphql`;
|
|
239
|
+
var RATE_LIMIT_SELECTION = "rateLimit { cost limit remaining resetAt }";
|
|
240
|
+
var readBody = async (response, maxBytes) => {
|
|
241
|
+
const stream = response.body;
|
|
242
|
+
if (stream === null) return { body: "", truncated: false };
|
|
243
|
+
const reader = stream.getReader();
|
|
244
|
+
const decoder = new TextDecoder();
|
|
245
|
+
let body = "";
|
|
246
|
+
let read = 0;
|
|
247
|
+
for (; ; ) {
|
|
248
|
+
const chunk = await reader.read();
|
|
249
|
+
if (chunk.done) break;
|
|
250
|
+
const value = chunk.value;
|
|
251
|
+
if (value === void 0) continue;
|
|
252
|
+
if (read + value.byteLength > maxBytes) {
|
|
253
|
+
body += decoder.decode(value.subarray(0, maxBytes - read), { stream: true });
|
|
254
|
+
await reader.cancel();
|
|
255
|
+
return { body, truncated: true };
|
|
256
|
+
}
|
|
257
|
+
read += value.byteLength;
|
|
258
|
+
body += decoder.decode(value, { stream: true });
|
|
259
|
+
}
|
|
260
|
+
return { body: body + decoder.decode(), truncated: false };
|
|
261
|
+
};
|
|
262
|
+
var number = (value) => {
|
|
263
|
+
if (value === null) return null;
|
|
264
|
+
const parsed = Number(value.trim());
|
|
265
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
266
|
+
};
|
|
267
|
+
var retryAtFrom = (headers) => {
|
|
268
|
+
const after = number(headers.get("Retry-After"));
|
|
269
|
+
if (after !== null && after >= 0) return Date.now() + after * 1e3;
|
|
270
|
+
const reset = number(headers.get("x-ratelimit-reset"));
|
|
271
|
+
return reset !== null && reset > 0 ? reset * 1e3 : null;
|
|
272
|
+
};
|
|
273
|
+
var rateLimited = (input) => new GitHubError({
|
|
274
|
+
code: "rate-limited",
|
|
275
|
+
operation: input.operation,
|
|
276
|
+
detail: input.detail,
|
|
277
|
+
...input.retryAt === null ? {} : { data: { retryAt: input.retryAt } }
|
|
278
|
+
});
|
|
279
|
+
var isRateLimitStatus = (status, headers) => {
|
|
280
|
+
if (status === 429) return true;
|
|
281
|
+
if (status !== 403) return false;
|
|
282
|
+
return number(headers.get("x-ratelimit-remaining")) === 0 || headers.get("Retry-After") !== null;
|
|
283
|
+
};
|
|
284
|
+
var statusError = (input) => {
|
|
285
|
+
if (isRateLimitStatus(input.status, input.headers)) {
|
|
286
|
+
return rateLimited({
|
|
287
|
+
operation: input.operation,
|
|
288
|
+
detail: `GitHub API rate limit exceeded (HTTP ${input.status}).`,
|
|
289
|
+
retryAt: retryAtFrom(input.headers)
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (input.status === 401 || input.status === 403) {
|
|
293
|
+
return new GitHubError({
|
|
294
|
+
code: "unauthenticated",
|
|
295
|
+
operation: input.operation,
|
|
296
|
+
detail: `GitHub refused the token (HTTP ${input.status}). Check the token and its \`repo\` scope in ${CARD}.`
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return new GitHubError({
|
|
300
|
+
code: "failed",
|
|
301
|
+
operation: input.operation,
|
|
302
|
+
detail: `GitHub answered HTTP ${input.status}.`
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
var restBudgetError = (operation, headers) => {
|
|
306
|
+
const limit = number(headers.get("x-ratelimit-limit"));
|
|
307
|
+
const remaining = number(headers.get("x-ratelimit-remaining"));
|
|
308
|
+
if (limit === null || limit <= 0 || remaining === null || remaining < 0) return null;
|
|
309
|
+
if (remaining >= limit * RESERVE_RATIO) return null;
|
|
310
|
+
return rateLimited({
|
|
311
|
+
operation,
|
|
312
|
+
detail: `GitHub API quota is down to ${remaining} of ${limit}.`,
|
|
313
|
+
retryAt: retryAtFrom(headers)
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
var requestError = (operation, error) => {
|
|
317
|
+
const name = error instanceof Error ? error.name : "";
|
|
318
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
319
|
+
return new GitHubError({
|
|
320
|
+
code: "failed",
|
|
321
|
+
operation,
|
|
322
|
+
detail: "GitHub did not answer before the timeout."
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return new GitHubError({
|
|
326
|
+
code: "failed",
|
|
327
|
+
operation,
|
|
328
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
var send = async (ctx, input) => {
|
|
332
|
+
const host = configuredHost(ctx);
|
|
333
|
+
const asked = input.host === void 0 ? null : normalizeHost(input.host);
|
|
334
|
+
if (asked !== null && asked.length > 0 && asked !== host) {
|
|
335
|
+
throw new GitHubError({
|
|
336
|
+
code: "unauthenticated",
|
|
337
|
+
operation: input.operation,
|
|
338
|
+
detail: `This repository lives on ${asked}, but ${CARD} is configured for ${host}.`
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const token = configuredToken(ctx);
|
|
342
|
+
if (token === null) {
|
|
343
|
+
throw new GitHubError({
|
|
344
|
+
code: "missing-tool",
|
|
345
|
+
operation: input.operation,
|
|
346
|
+
detail: `No GitHub token is configured. Add a personal access token with the \`repo\` scope in ${CARD}.`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
let response;
|
|
350
|
+
try {
|
|
351
|
+
response = await ctx.http(input.url(host), {
|
|
352
|
+
method: input.method ?? "GET",
|
|
353
|
+
headers: {
|
|
354
|
+
Authorization: `Bearer ${token}`,
|
|
355
|
+
Accept: input.accept ?? "application/vnd.github+json",
|
|
356
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
357
|
+
"Content-Type": "application/json"
|
|
358
|
+
},
|
|
359
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
360
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
throw requestError(input.operation, error);
|
|
364
|
+
}
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
await response.body?.cancel();
|
|
367
|
+
throw statusError({
|
|
368
|
+
operation: input.operation,
|
|
369
|
+
status: response.status,
|
|
370
|
+
headers: response.headers
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const budget = restBudgetError(input.operation, response.headers);
|
|
374
|
+
if (budget !== null) {
|
|
375
|
+
await response.body?.cancel();
|
|
376
|
+
throw budget;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const read = await readBody(response, input.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
380
|
+
return { ...read, headers: response.headers };
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw requestError(input.operation, error);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var callGitHubApi = (ctx, input) => send(ctx, { ...input, url: (host) => `${restBase(host)}/${input.path}` });
|
|
386
|
+
var ORG_SCOPES = /* @__PURE__ */ new Set(["read:org", "admin:org", "read:discussion", "admin:discussion"]);
|
|
387
|
+
var orgScopeByContext = /* @__PURE__ */ new WeakMap();
|
|
388
|
+
var probeOrgScope = async (ctx, host) => {
|
|
389
|
+
const result = await callGitHubApi(ctx, {
|
|
390
|
+
operation: "readTokenScopes",
|
|
391
|
+
path: "user",
|
|
392
|
+
...host === void 0 ? {} : { host }
|
|
393
|
+
});
|
|
394
|
+
const granted = result.headers.get("x-oauth-scopes");
|
|
395
|
+
if (granted === null) return false;
|
|
396
|
+
return granted.split(",").some((scope) => ORG_SCOPES.has(scope.trim().toLowerCase()));
|
|
397
|
+
};
|
|
398
|
+
var canReadOrg = (ctx, host) => {
|
|
399
|
+
const token = configuredToken(ctx);
|
|
400
|
+
if (token === null) return Promise.resolve(false);
|
|
401
|
+
const byToken = orgScopeByContext.get(ctx) ?? /* @__PURE__ */ new Map();
|
|
402
|
+
orgScopeByContext.set(ctx, byToken);
|
|
403
|
+
const cached = byToken.get(token);
|
|
404
|
+
if (cached !== void 0) return cached;
|
|
405
|
+
const probe = probeOrgScope(ctx, host).catch(() => {
|
|
406
|
+
byToken.delete(token);
|
|
407
|
+
return false;
|
|
408
|
+
});
|
|
409
|
+
byToken.set(token, probe);
|
|
410
|
+
return probe;
|
|
411
|
+
};
|
|
412
|
+
var readError = (operation, what) => new GitHubError({
|
|
413
|
+
code: "failed",
|
|
414
|
+
operation,
|
|
415
|
+
detail: `GitHub returned an unreadable ${what} response.`
|
|
416
|
+
});
|
|
417
|
+
var parse = (raw) => {
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse(raw);
|
|
420
|
+
} catch {
|
|
421
|
+
return void 0;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
425
|
+
var callGitHubJson = async (ctx, input) => {
|
|
426
|
+
const result = await callGitHubApi(ctx, input);
|
|
427
|
+
if (result.truncated) throw readError(input.operation, "JSON");
|
|
428
|
+
const parsed = parse(result.body.trim());
|
|
429
|
+
if (parsed === void 0) throw readError(input.operation, "JSON");
|
|
430
|
+
return parsed;
|
|
431
|
+
};
|
|
432
|
+
var graphQlBudgetError = (operation, data) => {
|
|
433
|
+
const rate = asObject(asObject(data)?.["rateLimit"]);
|
|
434
|
+
if (rate === null) return null;
|
|
435
|
+
const limit = typeof rate["limit"] === "number" ? rate["limit"] : null;
|
|
436
|
+
const remaining = typeof rate["remaining"] === "number" ? rate["remaining"] : null;
|
|
437
|
+
const cost = typeof rate["cost"] === "number" ? rate["cost"] : 0;
|
|
438
|
+
if (limit === null || limit <= 0 || remaining === null || remaining < 0) return null;
|
|
439
|
+
if (remaining - Math.max(1, cost) >= limit * RESERVE_RATIO) return null;
|
|
440
|
+
const resetAt = typeof rate["resetAt"] === "string" ? Date.parse(rate["resetAt"]) : Number.NaN;
|
|
441
|
+
return rateLimited({
|
|
442
|
+
operation,
|
|
443
|
+
detail: `GitHub GraphQL quota is down to ${remaining} of ${limit}.`,
|
|
444
|
+
retryAt: Number.isFinite(resetAt) ? resetAt : null
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
var INSUFFICIENT_SCOPES = "INSUFFICIENT_SCOPES";
|
|
448
|
+
var errorTypes = (errors) => errors.map((entry) => text(asObject(entry)?.["type"])?.toUpperCase() ?? "");
|
|
449
|
+
var errorPaths = (errors) => errors.flatMap((entry) => {
|
|
450
|
+
const path2 = asObject(entry)?.["path"];
|
|
451
|
+
return Array.isArray(path2) ? [path2.map((part) => String(part)).join(".")] : [];
|
|
452
|
+
});
|
|
453
|
+
var graphQlError = (operation, errors) => {
|
|
454
|
+
const types = errorTypes(errors);
|
|
455
|
+
const message = text(asObject(errors[0])?.["message"]) ?? "GitHub rejected the query.";
|
|
456
|
+
if (types.includes("RATE_LIMITED")) {
|
|
457
|
+
return rateLimited({ operation, detail: message, retryAt: null });
|
|
458
|
+
}
|
|
459
|
+
if (types.includes("FORBIDDEN") || types.includes("INSUFFICIENT_SCOPES")) {
|
|
460
|
+
return new GitHubError({
|
|
461
|
+
code: "unauthenticated",
|
|
462
|
+
operation,
|
|
463
|
+
detail: `${message} Check the token and its scopes in ${CARD}.`
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return new GitHubError({ code: "failed", operation, detail: message });
|
|
467
|
+
};
|
|
468
|
+
var callGitHubGraphQl = async (ctx, input) => {
|
|
469
|
+
const result = await send(ctx, {
|
|
470
|
+
operation: input.operation,
|
|
471
|
+
...input.host === void 0 ? {} : { host: input.host },
|
|
472
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
473
|
+
url: (host) => graphQlUrl(host),
|
|
474
|
+
method: "POST",
|
|
475
|
+
body: JSON.stringify({ query: input.query, variables: input.variables ?? {} })
|
|
476
|
+
});
|
|
477
|
+
if (result.truncated) throw readError(input.operation, "GraphQL");
|
|
478
|
+
const parsed = asObject(parse(result.body.trim()));
|
|
479
|
+
if (parsed === null) throw readError(input.operation, "GraphQL");
|
|
480
|
+
const data = parsed["data"];
|
|
481
|
+
const errors = parsed["errors"];
|
|
482
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
483
|
+
const droppable = asObject(data) !== null && errorTypes(errors).every((type) => type === INSUFFICIENT_SCOPES);
|
|
484
|
+
if (!droppable) throw graphQlError(input.operation, errors);
|
|
485
|
+
ctx.log.warn("github graphql dropped fields this token has no scope for", {
|
|
486
|
+
operation: input.operation,
|
|
487
|
+
paths: errorPaths(errors)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (asObject(data) === null) throw readError(input.operation, "GraphQL");
|
|
491
|
+
const budget = graphQlBudgetError(input.operation, data);
|
|
492
|
+
if (budget !== null) throw budget;
|
|
493
|
+
return data;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/githubMapping.ts
|
|
497
|
+
var asObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
498
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
499
|
+
var text2 = (value) => {
|
|
500
|
+
if (typeof value !== "string") return null;
|
|
501
|
+
const trimmed = value.trim();
|
|
502
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
503
|
+
};
|
|
504
|
+
var int = (value) => typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : null;
|
|
505
|
+
var at = (value, ...keys) => {
|
|
506
|
+
let current = value;
|
|
507
|
+
for (const key of keys) {
|
|
508
|
+
const object = asObject2(current);
|
|
509
|
+
if (object === null) return void 0;
|
|
510
|
+
current = object[key];
|
|
511
|
+
}
|
|
512
|
+
return current;
|
|
513
|
+
};
|
|
514
|
+
var nodes = (value) => asArray(at(value, "nodes"));
|
|
515
|
+
var nextCursorOf = (value) => at(value, "pageInfo", "hasNextPage") === true ? text2(at(value, "pageInfo", "endCursor")) : null;
|
|
516
|
+
var REACTION_BY_GITHUB = {
|
|
517
|
+
THUMBS_UP: "thumbs-up",
|
|
518
|
+
THUMBS_DOWN: "thumbs-down",
|
|
519
|
+
LAUGH: "laugh",
|
|
520
|
+
HOORAY: "hooray",
|
|
521
|
+
CONFUSED: "confused",
|
|
522
|
+
HEART: "heart",
|
|
523
|
+
ROCKET: "rocket",
|
|
524
|
+
EYES: "eyes"
|
|
525
|
+
};
|
|
526
|
+
var GITHUB_BY_REACTION = {
|
|
527
|
+
"thumbs-up": "THUMBS_UP",
|
|
528
|
+
"thumbs-down": "THUMBS_DOWN",
|
|
529
|
+
laugh: "LAUGH",
|
|
530
|
+
hooray: "HOORAY",
|
|
531
|
+
confused: "CONFUSED",
|
|
532
|
+
heart: "HEART",
|
|
533
|
+
rocket: "ROCKET",
|
|
534
|
+
eyes: "EYES"
|
|
535
|
+
};
|
|
536
|
+
var gitHubReactionContent = (content) => GITHUB_BY_REACTION[content];
|
|
537
|
+
var REVIEW_EVENT_BY_VERDICT = {
|
|
538
|
+
comment: "COMMENT",
|
|
539
|
+
approve: "APPROVE",
|
|
540
|
+
"request-changes": "REQUEST_CHANGES"
|
|
541
|
+
};
|
|
542
|
+
var gitHubReviewPosition = (position) => {
|
|
543
|
+
switch (position.kind) {
|
|
544
|
+
case "added":
|
|
545
|
+
return { line: position.newLine, side: "RIGHT" };
|
|
546
|
+
case "deleted":
|
|
547
|
+
return { line: position.oldLine, side: "LEFT" };
|
|
548
|
+
case "context":
|
|
549
|
+
return position.side === "left" ? { line: position.oldLine, side: "LEFT" } : { line: position.newLine, side: "RIGHT" };
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var toState = (raw) => {
|
|
553
|
+
if (text2(raw["mergedAt"]) !== null) return "merged";
|
|
554
|
+
switch (text2(raw["state"])?.toUpperCase()) {
|
|
555
|
+
case "MERGED":
|
|
556
|
+
return "merged";
|
|
557
|
+
case "CLOSED":
|
|
558
|
+
return "closed";
|
|
559
|
+
default:
|
|
560
|
+
return "open";
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
var toMergeability = (value) => {
|
|
564
|
+
switch (text2(value)?.toUpperCase()) {
|
|
565
|
+
case "MERGEABLE":
|
|
566
|
+
return "mergeable";
|
|
567
|
+
case "CONFLICTING":
|
|
568
|
+
return "conflicting";
|
|
569
|
+
default:
|
|
570
|
+
return "unknown";
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var toReviewDecision = (value) => {
|
|
574
|
+
switch (text2(value)?.toUpperCase()) {
|
|
575
|
+
case "APPROVED":
|
|
576
|
+
return "approved";
|
|
577
|
+
case "CHANGES_REQUESTED":
|
|
578
|
+
return "changes-requested";
|
|
579
|
+
case "REVIEW_REQUIRED":
|
|
580
|
+
return "review-required";
|
|
581
|
+
default:
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var toCheckStatus = (raw) => {
|
|
586
|
+
const status = text2(raw["status"])?.toUpperCase();
|
|
587
|
+
if (status !== void 0 && status !== "COMPLETED") return "pending";
|
|
588
|
+
switch (text2(raw["conclusion"] ?? raw["state"])?.toUpperCase()) {
|
|
589
|
+
case "SUCCESS":
|
|
590
|
+
return "success";
|
|
591
|
+
case "FAILURE":
|
|
592
|
+
case "ERROR":
|
|
593
|
+
case "TIMED_OUT":
|
|
594
|
+
case "STARTUP_FAILURE":
|
|
595
|
+
// 跑完了却要人手动介入的检查是拦路的,不是中性的。
|
|
596
|
+
case "ACTION_REQUIRED":
|
|
597
|
+
return "failure";
|
|
598
|
+
case "CANCELLED":
|
|
599
|
+
return "cancelled";
|
|
600
|
+
case "SKIPPED":
|
|
601
|
+
return "skipped";
|
|
602
|
+
case "PENDING":
|
|
603
|
+
case "EXPECTED":
|
|
604
|
+
return "pending";
|
|
605
|
+
default:
|
|
606
|
+
return "neutral";
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
var UNSET_TIMESTAMP = "0001-01-01T00:00:00Z";
|
|
610
|
+
var realTimestamp = (value) => {
|
|
611
|
+
const stamp = text2(value);
|
|
612
|
+
return stamp === null || stamp === UNSET_TIMESTAMP ? null : stamp;
|
|
613
|
+
};
|
|
614
|
+
var isAtLeastAsNew = (candidate, kept) => candidate === null ? kept === null : kept === null || candidate >= kept;
|
|
615
|
+
var dedupeChecks = (entries) => {
|
|
616
|
+
const newest = /* @__PURE__ */ new Map();
|
|
617
|
+
for (const entry of entries) {
|
|
618
|
+
const key = `${entry.workflowName ?? ""} ${entry.check.name}`;
|
|
619
|
+
const kept = newest.get(key);
|
|
620
|
+
if (kept === void 0 || isAtLeastAsNew(entry.at, kept.at)) newest.set(key, entry);
|
|
621
|
+
}
|
|
622
|
+
const survivors = [...newest.values()];
|
|
623
|
+
const countsByName = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const entry of survivors) {
|
|
625
|
+
countsByName.set(entry.check.name, (countsByName.get(entry.check.name) ?? 0) + 1);
|
|
626
|
+
}
|
|
627
|
+
return survivors.map((entry) => {
|
|
628
|
+
const workflowName = entry.workflowName ?? "";
|
|
629
|
+
return workflowName.length > 0 && (countsByName.get(entry.check.name) ?? 0) > 1 ? { ...entry.check, name: `${workflowName} / ${entry.check.name}` } : entry.check;
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
var toChecks = (contexts) => dedupeChecks(
|
|
633
|
+
contexts.flatMap((entry) => {
|
|
634
|
+
const raw = asObject2(entry);
|
|
635
|
+
if (raw === null) return [];
|
|
636
|
+
const name = text2(raw["name"]) ?? text2(raw["context"]);
|
|
637
|
+
if (name === null) return [];
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
640
|
+
check: {
|
|
641
|
+
name,
|
|
642
|
+
status: toCheckStatus(raw),
|
|
643
|
+
description: text2(raw["description"]) ?? text2(raw["title"]),
|
|
644
|
+
url: text2(raw["detailsUrl"]) ?? text2(raw["targetUrl"])
|
|
645
|
+
},
|
|
646
|
+
workflowName: text2(at(raw, "checkSuite", "workflowRun", "workflow", "name")),
|
|
647
|
+
at: realTimestamp(raw["completedAt"]) ?? realTimestamp(raw["startedAt"])
|
|
648
|
+
}
|
|
649
|
+
];
|
|
650
|
+
})
|
|
651
|
+
);
|
|
652
|
+
var toChecksState = (value) => {
|
|
653
|
+
switch (text2(value)?.toUpperCase()) {
|
|
654
|
+
case "FAILURE":
|
|
655
|
+
case "ERROR":
|
|
656
|
+
return "failing";
|
|
657
|
+
case "PENDING":
|
|
658
|
+
case "EXPECTED":
|
|
659
|
+
return "pending";
|
|
660
|
+
case "SUCCESS":
|
|
661
|
+
return "passing";
|
|
662
|
+
default:
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
var checksStateFromChecks = (checks) => {
|
|
667
|
+
const statuses = checks.map((check) => check.status);
|
|
668
|
+
if (statuses.length === 0) return null;
|
|
669
|
+
if (statuses.includes("failure")) return "failing";
|
|
670
|
+
if (statuses.includes("pending")) return "pending";
|
|
671
|
+
return statuses.includes("success") ? "passing" : null;
|
|
672
|
+
};
|
|
673
|
+
var toActor = (value) => {
|
|
674
|
+
const raw = asObject2(value);
|
|
675
|
+
const login = text2(raw?.["login"]);
|
|
676
|
+
return login === null ? null : { login, name: text2(raw?.["name"]), avatarUrl: text2(raw?.["avatarUrl"]) };
|
|
677
|
+
};
|
|
678
|
+
var toCommitActor = (value) => {
|
|
679
|
+
const raw = asObject2(value);
|
|
680
|
+
const login = text2(at(raw, "user", "login")) ?? text2(raw?.["name"]);
|
|
681
|
+
return login === null ? null : { login, name: text2(raw?.["name"]), avatarUrl: text2(raw?.["avatarUrl"]) };
|
|
682
|
+
};
|
|
683
|
+
var toLabels = (value) => nodes(value).flatMap((entry) => {
|
|
684
|
+
const name = text2(asObject2(entry)?.["name"]);
|
|
685
|
+
return name === null ? [] : [{ name, color: text2(asObject2(entry)?.["color"]) }];
|
|
686
|
+
});
|
|
687
|
+
var REACTORS_PER_GROUP = 10;
|
|
688
|
+
var toReactions = (value, viewer) => {
|
|
689
|
+
const normalizedViewer = viewer?.toLowerCase() ?? null;
|
|
690
|
+
const reactions = [];
|
|
691
|
+
for (const entry of asArray(value)) {
|
|
692
|
+
const group = asObject2(entry);
|
|
693
|
+
if (group === null) continue;
|
|
694
|
+
const content = REACTION_BY_GITHUB[text2(group["content"])?.toUpperCase() ?? ""];
|
|
695
|
+
if (content === void 0) continue;
|
|
696
|
+
const logins = nodes(group["reactors"]).flatMap(
|
|
697
|
+
(node) => text2(asObject2(node)?.["login"]) ?? []
|
|
698
|
+
);
|
|
699
|
+
const count = Math.max(
|
|
700
|
+
int(at(group, "reactors", "totalCount")) ?? logins.length,
|
|
701
|
+
logins.length
|
|
702
|
+
);
|
|
703
|
+
if (count <= 0) continue;
|
|
704
|
+
reactions.push({
|
|
705
|
+
content,
|
|
706
|
+
count,
|
|
707
|
+
actors: normalizedViewer === null ? logins : logins.filter((login) => login.toLowerCase() !== normalizedViewer),
|
|
708
|
+
viewerHasReacted: group["viewerHasReacted"] === true
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return reactions;
|
|
712
|
+
};
|
|
713
|
+
var PAGE_SIZE = 100;
|
|
714
|
+
var SEARCH_MAX_ROWS = PAGE_SIZE;
|
|
715
|
+
var reactionGroups = (canReadOrg2) => `reactionGroups {
|
|
716
|
+
content
|
|
717
|
+
viewerHasReacted
|
|
718
|
+
reactors(first: ${REACTORS_PER_GROUP}) {
|
|
719
|
+
totalCount
|
|
720
|
+
nodes { ... on User { login } ... on Bot { login } ... on Mannequin { login } ... on Organization { ${canReadOrg2 ? "login" : "__typename"} } }
|
|
721
|
+
}
|
|
722
|
+
}`;
|
|
723
|
+
var ACTOR_FIELDS = "login avatarUrl ... on User { name }";
|
|
724
|
+
var teamFragment = (canReadOrg2, fields) => `... on Team { __typename${canReadOrg2 ? ` ${fields}` : ""} }`;
|
|
725
|
+
var rowFields = (input) => `number
|
|
726
|
+
title
|
|
727
|
+
url
|
|
728
|
+
author { ${ACTOR_FIELDS} }
|
|
729
|
+
headRefName
|
|
730
|
+
baseRefName
|
|
731
|
+
state
|
|
732
|
+
isDraft
|
|
733
|
+
mergeable
|
|
734
|
+
reviewDecision
|
|
735
|
+
createdAt
|
|
736
|
+
updatedAt
|
|
737
|
+
mergedAt
|
|
738
|
+
${input.reviewRequests ? `reviewRequests(first: 20) { nodes { requestedReviewer { ... on User { login } ${teamFragment(input.canReadOrg, "slug")} } } }` : ""}
|
|
739
|
+
labels(first: 20) { nodes { name color } }
|
|
740
|
+
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }`;
|
|
741
|
+
var clampRows = (rows) => Math.min(Math.max(Math.trunc(rows), 1), SEARCH_MAX_ROWS);
|
|
742
|
+
var searchQueryDocument = (rows, canReadOrg2) => `query($q: String!) {
|
|
743
|
+
search(query: $q, type: ISSUE, first: ${clampRows(rows)}) {
|
|
744
|
+
pageInfo { hasNextPage }
|
|
745
|
+
nodes { ... on PullRequest { ${rowFields({ canReadOrg: canReadOrg2, reviewRequests: true })} repository { nameWithOwner } } }
|
|
746
|
+
}
|
|
747
|
+
${RATE_LIMIT_SELECTION}
|
|
748
|
+
}`;
|
|
749
|
+
var repositorySearchQueryDocument = (rows, canReadOrg2) => `query($q: String!) {
|
|
750
|
+
search(query: $q, type: ISSUE, first: ${clampRows(rows)}) {
|
|
751
|
+
pageInfo { hasNextPage }
|
|
752
|
+
nodes { ... on PullRequest { ${rowFields({ canReadOrg: canReadOrg2, reviewRequests: true })} additions deletions } }
|
|
753
|
+
}
|
|
754
|
+
${RATE_LIMIT_SELECTION}
|
|
755
|
+
}`;
|
|
756
|
+
var detailQueryDocument = (canReadOrg2) => `query($owner: String!, $name: String!, $number: Int!) {
|
|
757
|
+
repository(owner: $owner, name: $name) {
|
|
758
|
+
mergeCommitAllowed
|
|
759
|
+
squashMergeAllowed
|
|
760
|
+
rebaseMergeAllowed
|
|
761
|
+
viewerPermission
|
|
762
|
+
pullRequest(number: $number) {
|
|
763
|
+
${rowFields({ canReadOrg: canReadOrg2, reviewRequests: false })}
|
|
764
|
+
additions
|
|
765
|
+
deletions
|
|
766
|
+
body
|
|
767
|
+
changedFiles
|
|
768
|
+
closedAt
|
|
769
|
+
viewerCanUpdate
|
|
770
|
+
viewerDidAuthor
|
|
771
|
+
viewerCanUpdateBranch
|
|
772
|
+
autoMergeRequest { enabledAt }
|
|
773
|
+
headRepositoryOwner { login }
|
|
774
|
+
reviewRequests(first: 50) {
|
|
775
|
+
nodes { requestedReviewer { ... on User { login name avatarUrl } ... on Bot { login avatarUrl } ${teamFragment(canReadOrg2, "slug name avatarUrl")} } }
|
|
776
|
+
}
|
|
777
|
+
commits(last: 1) {
|
|
778
|
+
nodes {
|
|
779
|
+
commit {
|
|
780
|
+
statusCheckRollup {
|
|
781
|
+
contexts(first: ${PAGE_SIZE}) {
|
|
782
|
+
nodes {
|
|
783
|
+
... on CheckRun {
|
|
784
|
+
name status conclusion detailsUrl startedAt completedAt
|
|
785
|
+
checkSuite { workflowRun { workflow { name } } }
|
|
786
|
+
}
|
|
787
|
+
... on StatusContext { context state description targetUrl createdAt }
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
${RATE_LIMIT_SELECTION}
|
|
797
|
+
}`;
|
|
798
|
+
var BASE_COMPARISON_QUERY = `query($owner: String!, $name: String!, $number: Int!, $headRef: String!) {
|
|
799
|
+
repository(owner: $owner, name: $name) {
|
|
800
|
+
pullRequest(number: $number) {
|
|
801
|
+
viewerCanUpdateBranch
|
|
802
|
+
baseRef { compare(headRef: $headRef) { behindBy } }
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
${RATE_LIMIT_SELECTION}
|
|
806
|
+
}`;
|
|
807
|
+
var activityQueryDocument = (canReadOrg2) => `query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
|
|
808
|
+
viewer { login }
|
|
809
|
+
repository(owner: $owner, name: $name) {
|
|
810
|
+
pullRequest(number: $number) {
|
|
811
|
+
author { ${ACTOR_FIELDS} }
|
|
812
|
+
${reactionGroups(canReadOrg2)}
|
|
813
|
+
reviewThreads(first: ${PAGE_SIZE}, after: $cursor) {
|
|
814
|
+
pageInfo { hasNextPage endCursor }
|
|
815
|
+
nodes {
|
|
816
|
+
id isResolved isOutdated path line diffSide
|
|
817
|
+
comments(first: 10) {
|
|
818
|
+
totalCount
|
|
819
|
+
pageInfo { hasNextPage endCursor }
|
|
820
|
+
nodes { id author { ${ACTOR_FIELDS} } body createdAt url ${reactionGroups(canReadOrg2)} }
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
comments(first: ${PAGE_SIZE}) {
|
|
825
|
+
pageInfo { hasNextPage }
|
|
826
|
+
nodes { id author { ${ACTOR_FIELDS} } body createdAt url ${reactionGroups(canReadOrg2)} }
|
|
827
|
+
}
|
|
828
|
+
reviews(first: ${PAGE_SIZE}) {
|
|
829
|
+
pageInfo { hasNextPage }
|
|
830
|
+
nodes { id author { ${ACTOR_FIELDS} } body state submittedAt url ${reactionGroups(canReadOrg2)} }
|
|
831
|
+
}
|
|
832
|
+
reviewRequests(first: 50) {
|
|
833
|
+
nodes { requestedReviewer { ... on User { login name avatarUrl } ... on Bot { login avatarUrl } } }
|
|
834
|
+
}
|
|
835
|
+
latestReviews(first: 50) { nodes { author { ${ACTOR_FIELDS} } } }
|
|
836
|
+
reviewDismissals: timelineItems(itemTypes: [REVIEW_DISMISSED_EVENT], first: ${PAGE_SIZE}) {
|
|
837
|
+
nodes { ... on ReviewDismissedEvent { dismissalMessage review { id } } }
|
|
838
|
+
}
|
|
839
|
+
commits(last: ${PAGE_SIZE}) {
|
|
840
|
+
nodes {
|
|
841
|
+
commit {
|
|
842
|
+
oid messageHeadline committedDate additions deletions
|
|
843
|
+
authors(first: 3) { nodes { name avatarUrl user { login } } }
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
${RATE_LIMIT_SELECTION}
|
|
850
|
+
}`;
|
|
851
|
+
var threadCommentsQueryDocument = (canReadOrg2) => `query($owner: String!, $name: String!, $number: Int!, $threadId: ID!, $cursor: String) {
|
|
852
|
+
viewer { login }
|
|
853
|
+
repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }
|
|
854
|
+
node(id: $threadId) {
|
|
855
|
+
... on PullRequestReviewThread {
|
|
856
|
+
pullRequest { id }
|
|
857
|
+
comments(first: ${PAGE_SIZE}, after: $cursor) {
|
|
858
|
+
pageInfo { hasNextPage endCursor }
|
|
859
|
+
nodes { id author { ${ACTOR_FIELDS} } body createdAt url ${reactionGroups(canReadOrg2)} }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
${RATE_LIMIT_SELECTION}
|
|
864
|
+
}`;
|
|
865
|
+
var VIEWER_PERMISSIONS_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
866
|
+
repository(owner: $owner, name: $name) {
|
|
867
|
+
viewerPermission
|
|
868
|
+
pullRequest(number: $number) { viewerCanUpdate viewerDidAuthor viewerCanUpdateBranch }
|
|
869
|
+
}
|
|
870
|
+
${RATE_LIMIT_SELECTION}
|
|
871
|
+
}`;
|
|
872
|
+
var reviewerCandidatesQueryDocument = (canReadOrg2) => `query($owner: String!, $name: String!, $number: Int!) {
|
|
873
|
+
repository(owner: $owner, name: $name) {
|
|
874
|
+
assignableUsers(first: ${PAGE_SIZE}) {
|
|
875
|
+
pageInfo { hasNextPage }
|
|
876
|
+
nodes { login name avatarUrl }
|
|
877
|
+
}
|
|
878
|
+
pullRequest(number: $number) {
|
|
879
|
+
author { login }
|
|
880
|
+
reviewRequests(first: ${PAGE_SIZE}) {
|
|
881
|
+
nodes { requestedReviewer { ... on User { login name avatarUrl } ${teamFragment(canReadOrg2, "slug name avatarUrl")} ... on Bot { login avatarUrl } } }
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
${RATE_LIMIT_SELECTION}
|
|
886
|
+
}`;
|
|
887
|
+
var NODE_ID_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
888
|
+
repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }
|
|
889
|
+
${RATE_LIMIT_SELECTION}
|
|
890
|
+
}`;
|
|
891
|
+
var SUBJECT_SCOPE_QUERY = `query($owner: String!, $name: String!, $number: Int!, $subjectId: ID!) {
|
|
892
|
+
repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } }
|
|
893
|
+
node(id: $subjectId) {
|
|
894
|
+
id
|
|
895
|
+
... on IssueComment { pullRequest { id } }
|
|
896
|
+
... on PullRequestReviewComment { pullRequest { id } }
|
|
897
|
+
... on PullRequestReview { pullRequest { id } }
|
|
898
|
+
}
|
|
899
|
+
${RATE_LIMIT_SELECTION}
|
|
900
|
+
}`;
|
|
901
|
+
var REPLY_MUTATION = `mutation($threadId: ID!, $body: String!) {
|
|
902
|
+
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { comment { id } }
|
|
903
|
+
}`;
|
|
904
|
+
var RESOLVE_MUTATION = `mutation($threadId: ID!) {
|
|
905
|
+
resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
|
|
906
|
+
}`;
|
|
907
|
+
var UNRESOLVE_MUTATION = `mutation($threadId: ID!) {
|
|
908
|
+
unresolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
|
|
909
|
+
}`;
|
|
910
|
+
var ADD_REACTION_MUTATION = `mutation($subjectId: ID!, $content: ReactionContent!) {
|
|
911
|
+
addReaction(input: { subjectId: $subjectId, content: $content }) { reaction { content } }
|
|
912
|
+
}`;
|
|
913
|
+
var REMOVE_REACTION_MUTATION = `mutation($subjectId: ID!, $content: ReactionContent!) {
|
|
914
|
+
removeReaction(input: { subjectId: $subjectId, content: $content }) { reaction { content } }
|
|
915
|
+
}`;
|
|
916
|
+
var UPDATE_ISSUE_COMMENT_MUTATION = `mutation($commentId: ID!, $body: String!) {
|
|
917
|
+
updateIssueComment(input: { id: $commentId, body: $body }) { issueComment { id } }
|
|
918
|
+
}`;
|
|
919
|
+
var UPDATE_REVIEW_COMMENT_MUTATION = `mutation($commentId: ID!, $body: String!) {
|
|
920
|
+
updatePullRequestReviewComment(input: { pullRequestReviewCommentId: $commentId, body: $body }) {
|
|
921
|
+
pullRequestReviewComment { id }
|
|
922
|
+
}
|
|
923
|
+
}`;
|
|
924
|
+
var READY_MUTATION = `mutation($pullRequestId: ID!) {
|
|
925
|
+
markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) { pullRequest { id } }
|
|
926
|
+
}`;
|
|
927
|
+
var DRAFT_MUTATION = `mutation($pullRequestId: ID!) {
|
|
928
|
+
convertPullRequestToDraft(input: { pullRequestId: $pullRequestId }) { pullRequest { id } }
|
|
929
|
+
}`;
|
|
930
|
+
var UPDATE_BRANCH_MUTATION = `mutation($pullRequestId: ID!, $updateMethod: PullRequestBranchUpdateMethod) {
|
|
931
|
+
updatePullRequestBranch(input: { pullRequestId: $pullRequestId, updateMethod: $updateMethod }) {
|
|
932
|
+
pullRequest { id }
|
|
933
|
+
}
|
|
934
|
+
}`;
|
|
935
|
+
var ENABLE_AUTO_MERGE_MUTATION = `mutation($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod) {
|
|
936
|
+
enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: $mergeMethod }) {
|
|
937
|
+
pullRequest { id }
|
|
938
|
+
}
|
|
939
|
+
}`;
|
|
940
|
+
var DISABLE_AUTO_MERGE_MUTATION = `mutation($pullRequestId: ID!) {
|
|
941
|
+
disablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId }) { pullRequest { id } }
|
|
942
|
+
}`;
|
|
943
|
+
var REPOSITORY_PART = /^[A-Za-z0-9._-]+$/;
|
|
944
|
+
var buildStatsQuery = (changeRequests) => {
|
|
945
|
+
if (changeRequests.length === 0) return null;
|
|
946
|
+
const selections = [];
|
|
947
|
+
for (const [index, changeRequest] of changeRequests.entries()) {
|
|
948
|
+
const [owner, name, ...rest] = changeRequest.repository.trim().split("/");
|
|
949
|
+
if (rest.length > 0 || owner === void 0 || name === void 0) return null;
|
|
950
|
+
if (!REPOSITORY_PART.test(owner) || !REPOSITORY_PART.test(name)) return null;
|
|
951
|
+
if (!Number.isSafeInteger(changeRequest.number) || changeRequest.number <= 0) return null;
|
|
952
|
+
selections.push(
|
|
953
|
+
` s${index}: repository(owner: "${owner}", name: "${name}") { pullRequest(number: ${changeRequest.number}) { additions deletions } }`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
return `query {
|
|
957
|
+
${selections.join("\n")}
|
|
958
|
+
${RATE_LIMIT_SELECTION}
|
|
959
|
+
}`;
|
|
960
|
+
};
|
|
961
|
+
var decodeViewerLogin = (value) => text2(asObject2(value)?.["login"]);
|
|
962
|
+
var toReviewRequestLogins = (value) => nodes(value).flatMap((node) => text2(at(node, "requestedReviewer", "login")) ?? []);
|
|
963
|
+
var toRow = (raw) => {
|
|
964
|
+
const number2 = int(raw["number"]);
|
|
965
|
+
const title = text2(raw["title"]);
|
|
966
|
+
const url = text2(raw["url"]);
|
|
967
|
+
const headBranch = text2(raw["headRefName"]);
|
|
968
|
+
const baseBranch = text2(raw["baseRefName"]);
|
|
969
|
+
const createdAt = text2(raw["createdAt"]);
|
|
970
|
+
const updatedAt = text2(raw["updatedAt"]);
|
|
971
|
+
if (number2 === null || title === null || url === null || headBranch === null || baseBranch === null || createdAt === null || updatedAt === null) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
number: number2,
|
|
976
|
+
title,
|
|
977
|
+
url,
|
|
978
|
+
author: toActor(raw["author"]),
|
|
979
|
+
headBranch,
|
|
980
|
+
baseBranch,
|
|
981
|
+
state: toState(raw),
|
|
982
|
+
isDraft: raw["isDraft"] === true,
|
|
983
|
+
mergeability: toMergeability(raw["mergeable"]),
|
|
984
|
+
reviewDecision: toReviewDecision(raw["reviewDecision"]),
|
|
985
|
+
additions: int(raw["additions"]) ?? 0,
|
|
986
|
+
deletions: int(raw["deletions"]) ?? 0,
|
|
987
|
+
createdAt,
|
|
988
|
+
updatedAt,
|
|
989
|
+
reviewRequestLogins: toReviewRequestLogins(raw["reviewRequests"]),
|
|
990
|
+
labels: toLabels(raw["labels"]),
|
|
991
|
+
checksState: toChecksState(
|
|
992
|
+
at(nodes(raw["commits"])[0], "commit", "statusCheckRollup", "state")
|
|
993
|
+
)
|
|
994
|
+
};
|
|
995
|
+
};
|
|
996
|
+
var decodeSearch = (value) => {
|
|
997
|
+
const search = asObject2(at(value, "search"));
|
|
998
|
+
if (search === null) return null;
|
|
999
|
+
const raw = nodes(search);
|
|
1000
|
+
const items = [];
|
|
1001
|
+
for (const entry of raw) {
|
|
1002
|
+
const node = asObject2(entry);
|
|
1003
|
+
if (node === null) continue;
|
|
1004
|
+
const row = toRow(node);
|
|
1005
|
+
if (row === null) continue;
|
|
1006
|
+
items.push({ ...row, repository: text2(at(node, "repository", "nameWithOwner")) });
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
items,
|
|
1010
|
+
rawCount: raw.length,
|
|
1011
|
+
hasNextPage: at(search, "pageInfo", "hasNextPage") === true
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
var decodeRestPullList = (value) => {
|
|
1015
|
+
if (!Array.isArray(value)) return null;
|
|
1016
|
+
const items = [];
|
|
1017
|
+
for (const entry of value) {
|
|
1018
|
+
const raw = asObject2(entry);
|
|
1019
|
+
if (raw === null) continue;
|
|
1020
|
+
const number2 = int(raw["number"]);
|
|
1021
|
+
const title = text2(raw["title"]);
|
|
1022
|
+
const url = text2(raw["html_url"]);
|
|
1023
|
+
const headBranch = text2(at(raw, "head", "ref"));
|
|
1024
|
+
const baseBranch = text2(at(raw, "base", "ref"));
|
|
1025
|
+
const createdAt = text2(raw["created_at"]);
|
|
1026
|
+
const updatedAt = text2(raw["updated_at"]);
|
|
1027
|
+
if (number2 === null || title === null || url === null || headBranch === null || baseBranch === null || createdAt === null || updatedAt === null) {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const author = asObject2(raw["user"]);
|
|
1031
|
+
const login = text2(author?.["login"]);
|
|
1032
|
+
items.push({
|
|
1033
|
+
number: number2,
|
|
1034
|
+
title,
|
|
1035
|
+
url,
|
|
1036
|
+
author: login === null ? null : { login, name: null, avatarUrl: text2(author?.["avatar_url"]) },
|
|
1037
|
+
headBranch,
|
|
1038
|
+
baseBranch,
|
|
1039
|
+
state: toState({ state: raw["state"], mergedAt: raw["merged_at"] }),
|
|
1040
|
+
isDraft: raw["draft"] === true,
|
|
1041
|
+
mergeability: "unknown",
|
|
1042
|
+
reviewDecision: null,
|
|
1043
|
+
additions: 0,
|
|
1044
|
+
deletions: 0,
|
|
1045
|
+
createdAt,
|
|
1046
|
+
updatedAt,
|
|
1047
|
+
reviewRequestLogins: asArray(raw["requested_reviewers"]).flatMap(
|
|
1048
|
+
(reviewer) => text2(asObject2(reviewer)?.["login"]) ?? []
|
|
1049
|
+
),
|
|
1050
|
+
labels: asArray(raw["labels"]).flatMap((label) => {
|
|
1051
|
+
const name = text2(asObject2(label)?.["name"]);
|
|
1052
|
+
return name === null ? [] : [{ name, color: text2(asObject2(label)?.["color"]) }];
|
|
1053
|
+
}),
|
|
1054
|
+
checksState: null,
|
|
1055
|
+
hasTeamReviewRequest: asArray(raw["requested_teams"]).length > 0
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return { items, rawCount: value.length };
|
|
1059
|
+
};
|
|
1060
|
+
var toCanWrite = (value) => {
|
|
1061
|
+
switch (text2(value)?.toUpperCase()) {
|
|
1062
|
+
case "ADMIN":
|
|
1063
|
+
case "MAINTAIN":
|
|
1064
|
+
case "WRITE":
|
|
1065
|
+
return true;
|
|
1066
|
+
default:
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
var toViewerFields = (value) => ({
|
|
1071
|
+
canUpdate: asObject2(value)?.["viewerCanUpdate"] !== false,
|
|
1072
|
+
didAuthor: asObject2(value)?.["viewerDidAuthor"] === true
|
|
1073
|
+
});
|
|
1074
|
+
var decodeDetail = (value) => {
|
|
1075
|
+
const repository = asObject2(at(value, "repository"));
|
|
1076
|
+
const raw = asObject2(at(repository, "pullRequest"));
|
|
1077
|
+
if (repository === null || raw === null) return null;
|
|
1078
|
+
const row = toRow(raw);
|
|
1079
|
+
if (row === null) return null;
|
|
1080
|
+
const checks = toChecks(
|
|
1081
|
+
nodes(at(nodes(raw["commits"])[0], "commit", "statusCheckRollup", "contexts"))
|
|
1082
|
+
);
|
|
1083
|
+
return {
|
|
1084
|
+
detail: {
|
|
1085
|
+
...row,
|
|
1086
|
+
// 明细上有逐条检查,汇总就按它自己算,一个字和它下面那张表不会互相打脸。
|
|
1087
|
+
checksState: checksStateFromChecks(checks) ?? row.checksState,
|
|
1088
|
+
body: text2(raw["body"]) === null ? "" : String(raw["body"]),
|
|
1089
|
+
changedFiles: int(raw["changedFiles"]) ?? 0,
|
|
1090
|
+
mergedAt: text2(raw["mergedAt"]),
|
|
1091
|
+
closedAt: text2(raw["closedAt"]),
|
|
1092
|
+
reviewers: nodes(raw["reviewRequests"]).flatMap((node) => {
|
|
1093
|
+
const reviewer = asObject2(at(node, "requestedReviewer"));
|
|
1094
|
+
const login = text2(reviewer?.["login"]) ?? text2(reviewer?.["slug"]);
|
|
1095
|
+
return login === null ? [] : [{ login, name: text2(reviewer?.["name"]), avatarUrl: text2(reviewer?.["avatarUrl"]) }];
|
|
1096
|
+
}),
|
|
1097
|
+
checks,
|
|
1098
|
+
autoMergeEnabled: asObject2(raw["autoMergeRequest"]) !== null
|
|
1099
|
+
},
|
|
1100
|
+
mergeCapabilities: {
|
|
1101
|
+
merge: repository["mergeCommitAllowed"] === true,
|
|
1102
|
+
squash: repository["squashMergeAllowed"] === true,
|
|
1103
|
+
rebase: repository["rebaseMergeAllowed"] === true
|
|
1104
|
+
},
|
|
1105
|
+
access: {
|
|
1106
|
+
canWrite: toCanWrite(repository["viewerPermission"]),
|
|
1107
|
+
...toViewerFields(raw),
|
|
1108
|
+
canUpdateBranch: raw["viewerCanUpdateBranch"] === true
|
|
1109
|
+
},
|
|
1110
|
+
headRepositoryOwner: text2(at(raw, "headRepositoryOwner", "login"))
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
var decodeViewerAccess = (value) => {
|
|
1114
|
+
const repository = asObject2(at(value, "repository"));
|
|
1115
|
+
if (repository === null) return null;
|
|
1116
|
+
const pullRequest = repository["pullRequest"];
|
|
1117
|
+
return {
|
|
1118
|
+
canWrite: toCanWrite(repository["viewerPermission"]),
|
|
1119
|
+
...toViewerFields(pullRequest),
|
|
1120
|
+
canUpdateBranch: asObject2(pullRequest)?.["viewerCanUpdateBranch"] === true
|
|
1121
|
+
};
|
|
1122
|
+
};
|
|
1123
|
+
var decodeBaseComparison = (value) => {
|
|
1124
|
+
const pullRequest = asObject2(at(value, "repository", "pullRequest"));
|
|
1125
|
+
if (pullRequest === null) return null;
|
|
1126
|
+
const behindBy = int(at(pullRequest, "baseRef", "compare", "behindBy"));
|
|
1127
|
+
return {
|
|
1128
|
+
behindBy: behindBy !== null && behindBy >= 0 ? behindBy : null,
|
|
1129
|
+
viewerCanUpdate: pullRequest["viewerCanUpdateBranch"] === true
|
|
1130
|
+
};
|
|
1131
|
+
};
|
|
1132
|
+
var decodeStats = (value) => {
|
|
1133
|
+
const data = asObject2(value);
|
|
1134
|
+
if (data === null) return null;
|
|
1135
|
+
const stats = /* @__PURE__ */ new Map();
|
|
1136
|
+
for (const [alias, entry] of Object.entries(data)) {
|
|
1137
|
+
const index = /^s(\d+)$/.exec(alias)?.[1];
|
|
1138
|
+
const pullRequest = asObject2(at(entry, "pullRequest"));
|
|
1139
|
+
if (index === void 0 || pullRequest === null) continue;
|
|
1140
|
+
stats.set(Number(index), {
|
|
1141
|
+
additions: int(pullRequest["additions"]) ?? 0,
|
|
1142
|
+
deletions: int(pullRequest["deletions"]) ?? 0
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
return stats;
|
|
1146
|
+
};
|
|
1147
|
+
var isReviewVerdict = (state) => {
|
|
1148
|
+
switch (state?.toUpperCase()) {
|
|
1149
|
+
case "APPROVED":
|
|
1150
|
+
case "CHANGES_REQUESTED":
|
|
1151
|
+
case "DISMISSED":
|
|
1152
|
+
return true;
|
|
1153
|
+
default:
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
var rendersEmpty = (body) => body.replace(/<!--[\s\S]*?-->/g, "").trim().length === 0;
|
|
1158
|
+
var toThreadComment = (value, viewer) => {
|
|
1159
|
+
const raw = asObject2(value);
|
|
1160
|
+
const id = text2(raw?.["id"]);
|
|
1161
|
+
const createdAt = text2(raw?.["createdAt"]);
|
|
1162
|
+
if (id === null || createdAt === null) return null;
|
|
1163
|
+
return {
|
|
1164
|
+
id,
|
|
1165
|
+
author: toActor(raw?.["author"]),
|
|
1166
|
+
body: typeof raw?.["body"] === "string" ? raw["body"] : "",
|
|
1167
|
+
createdAt,
|
|
1168
|
+
url: text2(raw?.["url"]),
|
|
1169
|
+
reactions: toReactions(raw?.["reactionGroups"], viewer)
|
|
1170
|
+
};
|
|
1171
|
+
};
|
|
1172
|
+
var decodeActivity = (value) => {
|
|
1173
|
+
const raw = asObject2(at(value, "repository", "pullRequest"));
|
|
1174
|
+
if (raw === null) return null;
|
|
1175
|
+
const viewer = text2(at(value, "viewer", "login"));
|
|
1176
|
+
const threadPage = raw["reviewThreads"];
|
|
1177
|
+
let threadsTruncated = false;
|
|
1178
|
+
const reviewThreads = [];
|
|
1179
|
+
for (const entry of nodes(threadPage)) {
|
|
1180
|
+
const thread = asObject2(entry);
|
|
1181
|
+
const id = text2(thread?.["id"]);
|
|
1182
|
+
const path2 = text2(thread?.["path"]);
|
|
1183
|
+
const comments2 = nodes(thread?.["comments"]).flatMap(
|
|
1184
|
+
(comment3) => toThreadComment(comment3, viewer) ?? []
|
|
1185
|
+
);
|
|
1186
|
+
if (id === null || path2 === null || comments2.length === 0) continue;
|
|
1187
|
+
if (nextCursorOf(thread?.["comments"]) !== null) threadsTruncated = true;
|
|
1188
|
+
const line = int(thread?.["line"]);
|
|
1189
|
+
reviewThreads.push({
|
|
1190
|
+
id,
|
|
1191
|
+
path: path2,
|
|
1192
|
+
// 线程那一行离开 diff 之后 GitHub 就报 null,那正是它 outdated 的时候。这种线程照列,
|
|
1193
|
+
// 但不再钉在一条它已经没有的行上。
|
|
1194
|
+
line: line !== null && line > 0 ? line : null,
|
|
1195
|
+
side: text2(thread?.["diffSide"])?.toUpperCase() === "LEFT" ? "left" : "right",
|
|
1196
|
+
isResolved: thread?.["isResolved"] === true,
|
|
1197
|
+
isOutdated: thread?.["isOutdated"] === true,
|
|
1198
|
+
comments: comments2
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
const dismissals = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (const entry of nodes(raw["reviewDismissals"])) {
|
|
1203
|
+
const reviewId = text2(at(entry, "review", "id"));
|
|
1204
|
+
const message = text2(asObject2(entry)?.["dismissalMessage"]);
|
|
1205
|
+
if (reviewId !== null && message !== null) dismissals.set(reviewId, message);
|
|
1206
|
+
}
|
|
1207
|
+
const issueComments = nodes(raw["comments"]).flatMap(
|
|
1208
|
+
(entry) => {
|
|
1209
|
+
const comment3 = toThreadComment(entry, viewer);
|
|
1210
|
+
return comment3 === null ? [] : [{ ...comment3, kind: "issue-comment", path: null, reviewState: null }];
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
const reviews = nodes(raw["reviews"]).flatMap((entry) => {
|
|
1214
|
+
const review = asObject2(entry);
|
|
1215
|
+
const id = text2(review?.["id"]);
|
|
1216
|
+
const submittedAt = text2(review?.["submittedAt"]);
|
|
1217
|
+
const reviewState = text2(review?.["state"]);
|
|
1218
|
+
const body = typeof review?.["body"] === "string" ? review["body"] : "";
|
|
1219
|
+
if (id === null || submittedAt === null) return [];
|
|
1220
|
+
if (body.trim().length === 0 && !isReviewVerdict(reviewState)) return [];
|
|
1221
|
+
return [
|
|
1222
|
+
{
|
|
1223
|
+
id,
|
|
1224
|
+
kind: "review",
|
|
1225
|
+
author: toActor(review?.["author"]),
|
|
1226
|
+
// 撤销的理由写在时间线事件上而不是评审上,所以一条自己看不出话的撤销评审去那里
|
|
1227
|
+
// 取字。是"看不出"而不是"空":机器人的评审常常只有一个 HTML 标记,markdown 渲染
|
|
1228
|
+
// 出来什么都没有。
|
|
1229
|
+
body: reviewState?.toUpperCase() === "DISMISSED" && rendersEmpty(body) ? dismissals.get(id) ?? body : body,
|
|
1230
|
+
createdAt: submittedAt,
|
|
1231
|
+
url: text2(review?.["url"]),
|
|
1232
|
+
path: null,
|
|
1233
|
+
reviewState,
|
|
1234
|
+
reactions: toReactions(review?.["reactionGroups"], viewer)
|
|
1235
|
+
}
|
|
1236
|
+
];
|
|
1237
|
+
});
|
|
1238
|
+
const threadComments = reviewThreads.flatMap(
|
|
1239
|
+
(thread) => thread.comments.map(
|
|
1240
|
+
(comment3) => ({
|
|
1241
|
+
id: comment3.id,
|
|
1242
|
+
kind: "review-comment",
|
|
1243
|
+
author: comment3.author,
|
|
1244
|
+
body: comment3.body,
|
|
1245
|
+
createdAt: comment3.createdAt,
|
|
1246
|
+
url: comment3.url,
|
|
1247
|
+
path: thread.path,
|
|
1248
|
+
reviewState: null,
|
|
1249
|
+
reactions: comment3.reactions ?? []
|
|
1250
|
+
})
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
const reviewers = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const entry of [
|
|
1255
|
+
...nodes(raw["reviewRequests"]).map((node) => at(node, "requestedReviewer")),
|
|
1256
|
+
...nodes(raw["latestReviews"]).map((node) => at(node, "author"))
|
|
1257
|
+
]) {
|
|
1258
|
+
const actor = toActor(entry);
|
|
1259
|
+
if (actor !== null && !reviewers.has(actor.login)) reviewers.set(actor.login, actor);
|
|
1260
|
+
}
|
|
1261
|
+
const commits = nodes(raw["commits"]).flatMap((entry) => {
|
|
1262
|
+
const commit = asObject2(at(entry, "commit"));
|
|
1263
|
+
const oid = text2(commit?.["oid"]);
|
|
1264
|
+
const committedDate = text2(commit?.["committedDate"]);
|
|
1265
|
+
if (oid === null || committedDate === null) return [];
|
|
1266
|
+
const additions = int(commit?.["additions"]);
|
|
1267
|
+
const deletions = int(commit?.["deletions"]);
|
|
1268
|
+
return [
|
|
1269
|
+
{
|
|
1270
|
+
oid,
|
|
1271
|
+
messageHeadline: text2(commit?.["messageHeadline"]) ?? "",
|
|
1272
|
+
committedDate,
|
|
1273
|
+
...additions === null ? {} : { additions: Math.max(0, additions) },
|
|
1274
|
+
...deletions === null ? {} : { deletions: Math.max(0, deletions) },
|
|
1275
|
+
authors: nodes(commit?.["authors"]).flatMap((author) => toCommitActor(author) ?? [])
|
|
1276
|
+
}
|
|
1277
|
+
];
|
|
1278
|
+
});
|
|
1279
|
+
const comments = [...issueComments, ...reviews, ...threadComments].toSorted(
|
|
1280
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt)
|
|
1281
|
+
);
|
|
1282
|
+
return {
|
|
1283
|
+
activity: {
|
|
1284
|
+
author: toActor(raw["author"]),
|
|
1285
|
+
reviewers: [...reviewers.values()],
|
|
1286
|
+
reactions: toReactions(raw["reactionGroups"], viewer),
|
|
1287
|
+
comments,
|
|
1288
|
+
commentCount: comments.length,
|
|
1289
|
+
commentsTruncated: threadsTruncated || nextCursorOf(threadPage) !== null || at(raw, "comments", "pageInfo", "hasNextPage") === true || at(raw, "reviews", "pageInfo", "hasNextPage") === true,
|
|
1290
|
+
reviewThreads,
|
|
1291
|
+
commits
|
|
1292
|
+
},
|
|
1293
|
+
nextCursor: nextCursorOf(threadPage)
|
|
1294
|
+
};
|
|
1295
|
+
};
|
|
1296
|
+
var decodeThreadComments = (value) => {
|
|
1297
|
+
const data = asObject2(value);
|
|
1298
|
+
if (data === null) return null;
|
|
1299
|
+
const viewer = text2(at(data, "viewer", "login"));
|
|
1300
|
+
const expected = text2(at(data, "repository", "pullRequest", "id"));
|
|
1301
|
+
const actual = text2(at(data, "node", "pullRequest", "id"));
|
|
1302
|
+
const comments = at(data, "node", "comments");
|
|
1303
|
+
return {
|
|
1304
|
+
belongsToPullRequest: expected !== null && actual !== null && expected === actual,
|
|
1305
|
+
comments: nodes(comments).flatMap((entry) => toThreadComment(entry, viewer) ?? []),
|
|
1306
|
+
nextCursor: nextCursorOf(comments)
|
|
1307
|
+
};
|
|
1308
|
+
};
|
|
1309
|
+
var decodeNodeId = (value) => text2(at(value, "repository", "pullRequest", "id"));
|
|
1310
|
+
var decodeSubjectScope = (value) => {
|
|
1311
|
+
const expected = text2(at(value, "repository", "pullRequest", "id"));
|
|
1312
|
+
const node = asObject2(at(value, "node"));
|
|
1313
|
+
const actual = node === null ? null : text2(at(node, "pullRequest", "id")) ?? text2(node["id"]);
|
|
1314
|
+
return expected !== null && actual !== null && expected === actual;
|
|
1315
|
+
};
|
|
1316
|
+
var decodeReviewerCandidates = (value) => {
|
|
1317
|
+
const repository = asObject2(at(value, "repository"));
|
|
1318
|
+
if (repository === null) return null;
|
|
1319
|
+
const pullRequest = repository["pullRequest"];
|
|
1320
|
+
const author = text2(at(pullRequest, "author", "login"));
|
|
1321
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const entry of nodes(at(pullRequest, "reviewRequests"))) {
|
|
1323
|
+
const raw = asObject2(at(entry, "requestedReviewer"));
|
|
1324
|
+
const isTeam = raw?.["__typename"] === "Team";
|
|
1325
|
+
const id = isTeam ? text2(raw?.["slug"]) : text2(raw?.["login"]);
|
|
1326
|
+
if (id === null) continue;
|
|
1327
|
+
candidates.set(`${isTeam ? "team" : "user"} ${id}`, {
|
|
1328
|
+
id,
|
|
1329
|
+
kind: isTeam ? "team" : "user",
|
|
1330
|
+
login: id,
|
|
1331
|
+
name: text2(raw?.["name"]),
|
|
1332
|
+
avatarUrl: text2(raw?.["avatarUrl"]),
|
|
1333
|
+
isRequested: true
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const assignable = repository["assignableUsers"];
|
|
1337
|
+
for (const entry of nodes(assignable)) {
|
|
1338
|
+
const login = text2(asObject2(entry)?.["login"]);
|
|
1339
|
+
if (login === null || login === author || candidates.has(`user ${login}`)) continue;
|
|
1340
|
+
candidates.set(`user ${login}`, {
|
|
1341
|
+
id: login,
|
|
1342
|
+
kind: "user",
|
|
1343
|
+
login,
|
|
1344
|
+
name: text2(asObject2(entry)?.["name"]),
|
|
1345
|
+
avatarUrl: text2(asObject2(entry)?.["avatarUrl"]),
|
|
1346
|
+
isRequested: false
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
candidates: [...candidates.values()],
|
|
1351
|
+
truncated: at(assignable, "pageInfo", "hasNextPage") === true
|
|
1352
|
+
};
|
|
1353
|
+
};
|
|
1354
|
+
var decodeFiles = (value) => {
|
|
1355
|
+
if (!Array.isArray(value)) return null;
|
|
1356
|
+
const sections = [];
|
|
1357
|
+
const omittedFileStats = [];
|
|
1358
|
+
let truncated = false;
|
|
1359
|
+
for (const entry of value) {
|
|
1360
|
+
const raw = asObject2(entry);
|
|
1361
|
+
const filename = text2(raw?.["filename"]);
|
|
1362
|
+
if (filename === null) continue;
|
|
1363
|
+
const hunks = typeof raw?.["patch"] === "string" ? raw["patch"] : "";
|
|
1364
|
+
const status = text2(raw?.["status"])?.toLowerCase();
|
|
1365
|
+
if (hunks.length === 0) {
|
|
1366
|
+
const additions = int(raw?.["additions"]) ?? 0;
|
|
1367
|
+
const deletions = int(raw?.["deletions"]) ?? 0;
|
|
1368
|
+
if (additions + deletions > 0) {
|
|
1369
|
+
truncated = true;
|
|
1370
|
+
omittedFileStats.push({ path: filename, additions, deletions });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const oldPath = status === "renamed" ? text2(raw?.["previous_filename"]) ?? filename : filename;
|
|
1374
|
+
const header = [
|
|
1375
|
+
`diff --git a/${oldPath} b/${filename}`,
|
|
1376
|
+
// files 端点不报文件模式,用寻常那个顶上:查看器读这两行是"新增"和"删除",不是读模式。
|
|
1377
|
+
...status === "added" ? ["new file mode 100644"] : [],
|
|
1378
|
+
...status === "removed" ? ["deleted file mode 100644"] : [],
|
|
1379
|
+
...status === "renamed" ? [`rename from ${oldPath}`, `rename to ${filename}`] : [],
|
|
1380
|
+
`--- ${status === "added" ? "/dev/null" : `a/${oldPath}`}`,
|
|
1381
|
+
`+++ ${status === "removed" ? "/dev/null" : `b/${filename}`}`
|
|
1382
|
+
].join("\n");
|
|
1383
|
+
sections.push(hunks.length === 0 ? `${header}
|
|
1384
|
+
` : `${header}
|
|
1385
|
+
${hunks.replace(/\n?$/, "\n")}`);
|
|
1386
|
+
}
|
|
1387
|
+
return { patch: sections.join(""), truncated, rawCount: value.length, omittedFileStats };
|
|
1388
|
+
};
|
|
1389
|
+
var decodeRevisions = (value, kind) => {
|
|
1390
|
+
const raw = asObject2(value);
|
|
1391
|
+
if (raw === null) return null;
|
|
1392
|
+
return kind === "pull-request" ? { baseRef: text2(at(raw, "base", "sha")), headRef: text2(at(raw, "head", "sha")) } : { baseRef: text2(at(asArray(raw["parents"])[0], "sha")), headRef: text2(raw["sha"]) };
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// src/githubCommands.ts
|
|
1396
|
+
var CAPABILITIES = {
|
|
1397
|
+
diff: true,
|
|
1398
|
+
comment: true,
|
|
1399
|
+
actions: [
|
|
1400
|
+
"merge",
|
|
1401
|
+
"ready",
|
|
1402
|
+
"draft",
|
|
1403
|
+
"close",
|
|
1404
|
+
"reopen",
|
|
1405
|
+
"update-branch",
|
|
1406
|
+
"enable-auto-merge",
|
|
1407
|
+
"disable-auto-merge"
|
|
1408
|
+
],
|
|
1409
|
+
mergeMethods: ["merge", "squash", "rebase"],
|
|
1410
|
+
updateMethods: ["merge", "rebase"],
|
|
1411
|
+
search: true,
|
|
1412
|
+
reactions: true,
|
|
1413
|
+
review: {
|
|
1414
|
+
inlineComment: true,
|
|
1415
|
+
reply: true,
|
|
1416
|
+
resolve: true,
|
|
1417
|
+
verdicts: ["comment", "approve", "request-changes"]
|
|
1418
|
+
},
|
|
1419
|
+
reviewers: { request: true, listCandidates: true },
|
|
1420
|
+
edit: { changeRequest: true, comment: true }
|
|
1421
|
+
};
|
|
1422
|
+
var DIFF_MAX_BYTES = 8 * 1024 * 1024;
|
|
1423
|
+
var DIFF_TIMEOUT_MS = 6e4;
|
|
1424
|
+
var DIFF_FILE_MAX_BYTES = 1024 * 1024;
|
|
1425
|
+
var DIFF_FILES_PAGE_SIZE = 100;
|
|
1426
|
+
var REVIEW_THREAD_PAGES = 10;
|
|
1427
|
+
var FALLBACK_MAX_ROWS = 1e3;
|
|
1428
|
+
var REST_PAGE_SIZE = 100;
|
|
1429
|
+
var STAT_ALIASES_PER_REQUEST = 25;
|
|
1430
|
+
var STAT_REQUEST_CONCURRENCY = 4;
|
|
1431
|
+
var gitHubViewerPermissions = (access) => ({
|
|
1432
|
+
actions: [
|
|
1433
|
+
// 布置自动合并与撤销布置就是延后的合并:这里不能合的人,也不该留下一条以后合的指令。
|
|
1434
|
+
...access.canWrite ? ["merge", "enable-auto-merge", "disable-auto-merge"] : [],
|
|
1435
|
+
...access.canUpdate ? ["ready", "draft", "close", "reopen"] : [],
|
|
1436
|
+
// 能不能更新分支是 GitHub 自己的答案;答不上来就谁都不给,而不是谁都给。
|
|
1437
|
+
...access.canUpdateBranch ? ["update-branch"] : []
|
|
1438
|
+
],
|
|
1439
|
+
comment: true,
|
|
1440
|
+
resolve: access.canWrite || access.didAuthor,
|
|
1441
|
+
// 谁都能评审自己看得见的合并请求,除了自己那条:GitHub 拒绝作者的批准与要求修改
|
|
1442
|
+
// ("Can not approve your own pull request"),只留下评论——那也正是作者对自己改动能说的。
|
|
1443
|
+
verdicts: access.didAuthor ? ["comment"] : CAPABILITIES.review.verdicts,
|
|
1444
|
+
requestReviewers: access.canWrite,
|
|
1445
|
+
...access.canUpdateBranch ? { updateMethods: CAPABILITIES.updateMethods } : {}
|
|
1446
|
+
});
|
|
1447
|
+
var failed = (operation, detail) => new GitHubError({ code: "failed", operation, detail });
|
|
1448
|
+
var parseRepositorySelector = (value) => {
|
|
1449
|
+
const parts = value.trim().split("/").filter(Boolean);
|
|
1450
|
+
return { name: parts.at(-1) ?? "", owner: parts.at(-2) ?? "" };
|
|
1451
|
+
};
|
|
1452
|
+
var selector = (input, operation) => {
|
|
1453
|
+
const parsed = parseRepositorySelector(input.repository);
|
|
1454
|
+
if (parsed.owner.length === 0 || parsed.name.length === 0) {
|
|
1455
|
+
throw failed(operation, "A repository was named that GitHub cannot address.");
|
|
1456
|
+
}
|
|
1457
|
+
return parsed;
|
|
1458
|
+
};
|
|
1459
|
+
var path = (value) => value.split("/").map(encodeURIComponent).join("/");
|
|
1460
|
+
var repositoryPath = (input, operation) => {
|
|
1461
|
+
const { owner, name } = selector(input, operation);
|
|
1462
|
+
return `repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
|
1463
|
+
};
|
|
1464
|
+
var diffCursorPage = (cursor) => /^[1-9][0-9]{0,6}$/.test(cursor) ? Number(cursor) : null;
|
|
1465
|
+
var isCommitSha = (value) => /^[0-9a-f]{7,64}$/i.test(value);
|
|
1466
|
+
var searchPhrase = (query2) => `"${query2.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
1467
|
+
var REVIEW_QUALIFIERS = {
|
|
1468
|
+
approved: "approved",
|
|
1469
|
+
"changes-requested": "changes_requested",
|
|
1470
|
+
"review-required": "required",
|
|
1471
|
+
none: "none"
|
|
1472
|
+
};
|
|
1473
|
+
var qualifierValue = (value) => `"${value.replaceAll('"', "").trim()}"`;
|
|
1474
|
+
var resolveAuthor = (author, viewer) => {
|
|
1475
|
+
const trimmed = author.trim();
|
|
1476
|
+
if (!/^@?me$/i.test(trimmed)) return trimmed;
|
|
1477
|
+
return viewer.trim().length === 0 ? trimmed : viewer;
|
|
1478
|
+
};
|
|
1479
|
+
var filterQualifiers = (filters, viewer) => {
|
|
1480
|
+
if (filters === void 0) return [];
|
|
1481
|
+
return [
|
|
1482
|
+
// 一组一个限定词,组里的名字用逗号连起来——那是 GitHub 自己的"或"。
|
|
1483
|
+
...(filters.labels ?? []).flatMap(
|
|
1484
|
+
(group) => group.length === 0 ? [] : [`label:${group.map(qualifierValue).join(",")}`]
|
|
1485
|
+
),
|
|
1486
|
+
...(filters.excludedLabels ?? []).map((label) => `-label:${qualifierValue(label)}`),
|
|
1487
|
+
...filters.author === void 0 ? [] : [`author:${qualifierValue(resolveAuthor(filters.author, viewer))}`],
|
|
1488
|
+
...filters.draft === void 0 ? [] : [`draft:${filters.draft === "only"}`],
|
|
1489
|
+
...filters.review === void 0 || REVIEW_QUALIFIERS[filters.review] === void 0 ? [] : [`review:${REVIEW_QUALIFIERS[filters.review]}`],
|
|
1490
|
+
...filters.checks === void 0 ? [] : [`status:${filters.checks === "passing" ? "success" : "failure"}`]
|
|
1491
|
+
];
|
|
1492
|
+
};
|
|
1493
|
+
var SEARCH_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
1494
|
+
var searchQuery = (input) => {
|
|
1495
|
+
if (input.repositories.length === 0) return null;
|
|
1496
|
+
const repositories = input.repositories.map((repository) => repository.trim());
|
|
1497
|
+
if (!repositories.every((repository) => SEARCH_REPOSITORY.test(repository))) return null;
|
|
1498
|
+
const query2 = input.query?.trim() ?? "";
|
|
1499
|
+
return [
|
|
1500
|
+
"is:pr",
|
|
1501
|
+
// "all" 是所有状态,`is:pr` 本来就是。
|
|
1502
|
+
...input.state === "open" ? ["is:open"] : [],
|
|
1503
|
+
...input.state === "closed" ? ["is:closed", "is:unmerged"] : [],
|
|
1504
|
+
...input.state === "merged" ? ["is:merged"] : [],
|
|
1505
|
+
...input.involvement === "authored" ? [`author:${input.viewer}`] : [],
|
|
1506
|
+
...input.involvement === "reviewing" ? [`review-requested:${input.viewer}`] : [],
|
|
1507
|
+
...query2.length === 0 ? [] : [searchPhrase(query2)],
|
|
1508
|
+
// 上一片结束的那一刻,以及它之前的全部。闭区间:好几行共用一个时刻是常事,问严格更老
|
|
1509
|
+
// 会把其中一些丢掉——调用方自己去掉已经发过的那些。
|
|
1510
|
+
...input.cursor === void 0 ? [] : [`updated:<=${input.cursor.updatedBefore}`],
|
|
1511
|
+
...filterQualifiers(input.filters, input.viewer),
|
|
1512
|
+
// 页面读行的次序,也是续读唯一能接上的次序。
|
|
1513
|
+
"sort:updated-desc",
|
|
1514
|
+
...repositories.map((repository) => `repo:${repository}`)
|
|
1515
|
+
].join(" ");
|
|
1516
|
+
};
|
|
1517
|
+
var matchesFallback = (item, input) => {
|
|
1518
|
+
const viewer = input.viewer.toLowerCase();
|
|
1519
|
+
const matchesState = input.state === "all" || item.state === input.state;
|
|
1520
|
+
const matchesInvolvement = input.involvement === "all" || (input.involvement === "authored" ? item.author?.login.toLowerCase() === viewer : item.hasTeamReviewRequest || item.reviewRequestLogins.some((login) => login.toLowerCase() === viewer));
|
|
1521
|
+
const filters = input.filters;
|
|
1522
|
+
if (!matchesState || !matchesInvolvement) return false;
|
|
1523
|
+
if (filters === void 0) return true;
|
|
1524
|
+
const labels = new Set(item.labels.map((label) => label.name.trim().toLowerCase()));
|
|
1525
|
+
const holds = (label) => labels.has(label.trim().toLowerCase());
|
|
1526
|
+
return (filters.draft === void 0 || item.isDraft === (filters.draft === "only")) && (filters.labels === void 0 || filters.labels.every((group) => group.some(holds))) && (filters.excludedLabels === void 0 || !filters.excludedLabels.some(holds)) && (filters.author === void 0 || item.author?.login.toLowerCase() === resolveAuthor(filters.author, input.viewer).toLowerCase());
|
|
1527
|
+
};
|
|
1528
|
+
var getViewer = async (ctx) => {
|
|
1529
|
+
const login = decodeViewerLogin(
|
|
1530
|
+
await callGitHubJson(ctx, { operation: "getViewer", path: "user" })
|
|
1531
|
+
);
|
|
1532
|
+
if (login === null) {
|
|
1533
|
+
throw failed("getViewer", "GitHub returned no login for the authenticated account.");
|
|
1534
|
+
}
|
|
1535
|
+
return login;
|
|
1536
|
+
};
|
|
1537
|
+
var searchRows = (limit) => Math.min(limit + 1, SEARCH_MAX_ROWS);
|
|
1538
|
+
var listFallback = async (ctx, input) => {
|
|
1539
|
+
const repository = repositoryPath(input, "listChangeRequests");
|
|
1540
|
+
const collected = [];
|
|
1541
|
+
let raw = 0;
|
|
1542
|
+
for (let page = 1; raw < FALLBACK_MAX_ROWS; page += 1) {
|
|
1543
|
+
const decoded = decodeRestPullList(
|
|
1544
|
+
await callGitHubJson(ctx, {
|
|
1545
|
+
operation: "listChangeRequests",
|
|
1546
|
+
host: input.host,
|
|
1547
|
+
path: `${repository}/pulls?state=${input.state === "open" ? "open" : input.state === "all" ? "all" : "closed"}&sort=updated&direction=desc&per_page=${REST_PAGE_SIZE}&page=${page}`
|
|
1548
|
+
})
|
|
1549
|
+
);
|
|
1550
|
+
if (decoded === null) throw readError("listChangeRequests", "pull request list");
|
|
1551
|
+
raw += decoded.rawCount;
|
|
1552
|
+
for (const item of decoded.items) {
|
|
1553
|
+
if (!matchesFallback(item, input)) continue;
|
|
1554
|
+
const { hasTeamReviewRequest: _team, ...row } = item;
|
|
1555
|
+
collected.push(row);
|
|
1556
|
+
}
|
|
1557
|
+
if (collected.length > input.limit) {
|
|
1558
|
+
return { items: collected.slice(0, input.limit), truncated: true, continues: false };
|
|
1559
|
+
}
|
|
1560
|
+
if (decoded.rawCount < REST_PAGE_SIZE) {
|
|
1561
|
+
return { items: collected, truncated: false, continues: false };
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return { items: collected.slice(0, input.limit), truncated: true, continues: false };
|
|
1565
|
+
};
|
|
1566
|
+
var listChangeRequests = async (ctx, input) => {
|
|
1567
|
+
const query2 = searchQuery({ ...input, repositories: [input.repository] });
|
|
1568
|
+
if (query2 === null) {
|
|
1569
|
+
throw failed("listChangeRequests", "A repository was named that GitHub cannot address.");
|
|
1570
|
+
}
|
|
1571
|
+
const decoded = decodeSearch(
|
|
1572
|
+
await callGitHubGraphQl(ctx, {
|
|
1573
|
+
operation: "listChangeRequests",
|
|
1574
|
+
host: input.host,
|
|
1575
|
+
query: repositorySearchQueryDocument(
|
|
1576
|
+
searchRows(input.limit),
|
|
1577
|
+
await canReadOrg(ctx, input.host)
|
|
1578
|
+
),
|
|
1579
|
+
variables: { q: query2 }
|
|
1580
|
+
})
|
|
1581
|
+
);
|
|
1582
|
+
if (decoded === null) throw readError("listChangeRequests", "pull request search");
|
|
1583
|
+
const items = decoded.items.slice(0, input.limit).map(({ repository: _repository, ...row }) => row);
|
|
1584
|
+
const hasQuery = (input.query?.trim().length ?? 0) > 0;
|
|
1585
|
+
if (items.length === 0 && input.cursor === void 0 && !hasQuery) {
|
|
1586
|
+
return listFallback(ctx, input);
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
items,
|
|
1590
|
+
truncated: decoded.rawCount > input.limit || decoded.hasNextPage,
|
|
1591
|
+
continues: true
|
|
1592
|
+
};
|
|
1593
|
+
};
|
|
1594
|
+
var listChangeRequestsAcross = async (ctx, input) => {
|
|
1595
|
+
const query2 = searchQuery(input);
|
|
1596
|
+
if (query2 === null) {
|
|
1597
|
+
throw failed("listChangeRequestsAcross", "A repository was named that GitHub cannot address.");
|
|
1598
|
+
}
|
|
1599
|
+
const decoded = decodeSearch(
|
|
1600
|
+
await callGitHubGraphQl(ctx, {
|
|
1601
|
+
operation: "listChangeRequestsAcross",
|
|
1602
|
+
host: input.host,
|
|
1603
|
+
query: searchQueryDocument(searchRows(input.limit), await canReadOrg(ctx, input.host)),
|
|
1604
|
+
variables: { q: query2 }
|
|
1605
|
+
})
|
|
1606
|
+
);
|
|
1607
|
+
if (decoded === null) throw readError("listChangeRequestsAcross", "pull request search");
|
|
1608
|
+
return {
|
|
1609
|
+
items: decoded.items.slice(0, input.limit).flatMap((row) => row.repository === null ? [] : [{ ...row, repository: row.repository }]),
|
|
1610
|
+
truncated: decoded.rawCount > input.limit || decoded.hasNextPage
|
|
1611
|
+
};
|
|
1612
|
+
};
|
|
1613
|
+
var listChangeRequestStats = async (ctx, input) => {
|
|
1614
|
+
const chunks = [];
|
|
1615
|
+
for (let start = 0; start < input.changeRequests.length; start += STAT_ALIASES_PER_REQUEST) {
|
|
1616
|
+
chunks.push(input.changeRequests.slice(start, start + STAT_ALIASES_PER_REQUEST));
|
|
1617
|
+
}
|
|
1618
|
+
const read = async (chunk) => {
|
|
1619
|
+
const query2 = buildStatsQuery(chunk);
|
|
1620
|
+
if (query2 === null) {
|
|
1621
|
+
throw failed("listChangeRequestStats", "A repository was named that GitHub cannot address.");
|
|
1622
|
+
}
|
|
1623
|
+
const stats = decodeStats(
|
|
1624
|
+
await callGitHubGraphQl(ctx, {
|
|
1625
|
+
operation: "listChangeRequestStats",
|
|
1626
|
+
host: input.host,
|
|
1627
|
+
query: query2
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
if (stats === null) throw readError("listChangeRequestStats", "pull request stats");
|
|
1631
|
+
return chunk.flatMap((changeRequest, index) => {
|
|
1632
|
+
const stat = stats.get(index);
|
|
1633
|
+
return stat === void 0 ? [] : [{ ...changeRequest, ...stat }];
|
|
1634
|
+
});
|
|
1635
|
+
};
|
|
1636
|
+
const results = [];
|
|
1637
|
+
for (let start = 0; start < chunks.length; start += STAT_REQUEST_CONCURRENCY) {
|
|
1638
|
+
const batch = await Promise.all(
|
|
1639
|
+
chunks.slice(start, start + STAT_REQUEST_CONCURRENCY).map(read)
|
|
1640
|
+
);
|
|
1641
|
+
for (const entry of batch) results.push(...entry);
|
|
1642
|
+
}
|
|
1643
|
+
return results;
|
|
1644
|
+
};
|
|
1645
|
+
var detailOf = async (ctx, input, operation) => {
|
|
1646
|
+
const { owner, name } = selector(input, operation);
|
|
1647
|
+
const decoded = decodeDetail(
|
|
1648
|
+
await callGitHubGraphQl(ctx, {
|
|
1649
|
+
operation,
|
|
1650
|
+
host: input.host,
|
|
1651
|
+
query: detailQueryDocument(await canReadOrg(ctx, input.host)),
|
|
1652
|
+
variables: { owner, name, number: input.number }
|
|
1653
|
+
})
|
|
1654
|
+
);
|
|
1655
|
+
if (decoded === null) throw readError(operation, "pull request detail");
|
|
1656
|
+
return decoded;
|
|
1657
|
+
};
|
|
1658
|
+
var getChangeRequest = async (ctx, input) => {
|
|
1659
|
+
const { owner, name } = selector(input, "getChangeRequest");
|
|
1660
|
+
const read = await detailOf(ctx, input, "getChangeRequest");
|
|
1661
|
+
const comparison = read.detail.state !== "open" || read.headRepositoryOwner === null ? null : await callGitHubGraphQl(ctx, {
|
|
1662
|
+
operation: "getChangeRequest",
|
|
1663
|
+
host: input.host,
|
|
1664
|
+
query: BASE_COMPARISON_QUERY,
|
|
1665
|
+
variables: {
|
|
1666
|
+
owner,
|
|
1667
|
+
name,
|
|
1668
|
+
number: input.number,
|
|
1669
|
+
headRef: `${read.headRepositoryOwner}:${read.detail.headBranch}`
|
|
1670
|
+
}
|
|
1671
|
+
}).then(decodeBaseComparison).catch(() => null);
|
|
1672
|
+
return {
|
|
1673
|
+
...read.detail,
|
|
1674
|
+
mergeCapabilities: read.mergeCapabilities,
|
|
1675
|
+
viewerPermissions: gitHubViewerPermissions({
|
|
1676
|
+
...read.access,
|
|
1677
|
+
canUpdateBranch: comparison?.viewerCanUpdate === true
|
|
1678
|
+
}),
|
|
1679
|
+
baseComparison: comparison === null || comparison.behindBy === null ? "unknown" : comparison.behindBy > 0 ? "behind" : "up-to-date",
|
|
1680
|
+
...comparison?.behindBy == null ? {} : { behindBy: comparison.behindBy }
|
|
1681
|
+
};
|
|
1682
|
+
};
|
|
1683
|
+
var getChangeRequestActivity = async (ctx, input) => {
|
|
1684
|
+
const { owner, name } = selector(input, "getChangeRequestActivity");
|
|
1685
|
+
const query2 = activityQueryDocument(await canReadOrg(ctx, input.host));
|
|
1686
|
+
const readPage = async (cursor2) => {
|
|
1687
|
+
const decoded = decodeActivity(
|
|
1688
|
+
await callGitHubGraphQl(ctx, {
|
|
1689
|
+
operation: "getChangeRequestActivity",
|
|
1690
|
+
host: input.host,
|
|
1691
|
+
query: query2,
|
|
1692
|
+
variables: { owner, name, number: input.number, cursor: cursor2 }
|
|
1693
|
+
})
|
|
1694
|
+
);
|
|
1695
|
+
if (decoded === null) throw readError("getChangeRequestActivity", "pull request activity");
|
|
1696
|
+
return decoded;
|
|
1697
|
+
};
|
|
1698
|
+
const first = await readPage(null);
|
|
1699
|
+
const comments = [...first.activity.comments];
|
|
1700
|
+
const reviewThreads = [...first.activity.reviewThreads];
|
|
1701
|
+
let truncated = first.activity.commentsTruncated;
|
|
1702
|
+
let cursor = first.nextCursor;
|
|
1703
|
+
for (let page = 1; cursor !== null && page < REVIEW_THREAD_PAGES; page += 1) {
|
|
1704
|
+
const next = await readPage(cursor);
|
|
1705
|
+
reviewThreads.push(...next.activity.reviewThreads);
|
|
1706
|
+
comments.push(...next.activity.comments.filter((comment3) => comment3.kind === "review-comment"));
|
|
1707
|
+
truncated = truncated || next.activity.commentsTruncated;
|
|
1708
|
+
cursor = next.nextCursor;
|
|
1709
|
+
}
|
|
1710
|
+
const sorted = comments.toSorted((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
1711
|
+
return {
|
|
1712
|
+
...first.activity,
|
|
1713
|
+
comments: sorted,
|
|
1714
|
+
commentCount: sorted.length,
|
|
1715
|
+
commentsTruncated: truncated || cursor !== null,
|
|
1716
|
+
reviewThreads
|
|
1717
|
+
};
|
|
1718
|
+
};
|
|
1719
|
+
var getReviewThreadComments = async (ctx, input) => {
|
|
1720
|
+
const { owner, name } = selector(input, "getReviewThreadComments");
|
|
1721
|
+
const decoded = decodeThreadComments(
|
|
1722
|
+
await callGitHubGraphQl(ctx, {
|
|
1723
|
+
operation: "getReviewThreadComments",
|
|
1724
|
+
host: input.host,
|
|
1725
|
+
query: threadCommentsQueryDocument(await canReadOrg(ctx, input.host)),
|
|
1726
|
+
variables: {
|
|
1727
|
+
owner,
|
|
1728
|
+
name,
|
|
1729
|
+
number: input.number,
|
|
1730
|
+
threadId: input.threadId,
|
|
1731
|
+
cursor: input.cursor
|
|
1732
|
+
}
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1735
|
+
if (decoded === null) throw readError("getReviewThreadComments", "review thread");
|
|
1736
|
+
if (!decoded.belongsToPullRequest) {
|
|
1737
|
+
throw failed(
|
|
1738
|
+
"getReviewThreadComments",
|
|
1739
|
+
"The named subject did not belong to the named pull request."
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
return { comments: decoded.comments, nextCursor: decoded.nextCursor };
|
|
1743
|
+
};
|
|
1744
|
+
var getViewerPermissions = async (ctx, input) => {
|
|
1745
|
+
const { owner, name } = selector(input, "getViewerPermissions");
|
|
1746
|
+
const access = decodeViewerAccess(
|
|
1747
|
+
await callGitHubGraphQl(ctx, {
|
|
1748
|
+
operation: "getViewerPermissions",
|
|
1749
|
+
host: input.host,
|
|
1750
|
+
query: VIEWER_PERMISSIONS_QUERY,
|
|
1751
|
+
variables: { owner, name, number: input.number }
|
|
1752
|
+
})
|
|
1753
|
+
);
|
|
1754
|
+
if (access === null) throw readError("getViewerPermissions", "viewer permissions");
|
|
1755
|
+
return gitHubViewerPermissions(access);
|
|
1756
|
+
};
|
|
1757
|
+
var diffFilesPage = async (ctx, input) => {
|
|
1758
|
+
const repository = repositoryPath(input, "getDiff");
|
|
1759
|
+
const paging = `per_page=${DIFF_FILES_PAGE_SIZE}&page=${input.page}`;
|
|
1760
|
+
const result = await callGitHubApi(ctx, {
|
|
1761
|
+
operation: "getDiff",
|
|
1762
|
+
host: input.host,
|
|
1763
|
+
path: input.commit === void 0 ? `${repository}/pulls/${input.number}/files?${paging}` : `${repository}/commits/${input.commit}?${paging}`,
|
|
1764
|
+
maxBytes: DIFF_MAX_BYTES,
|
|
1765
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1766
|
+
});
|
|
1767
|
+
if (result.truncated) {
|
|
1768
|
+
throw failed("getDiff", `Page ${input.page} of the changed files was too large to read.`);
|
|
1769
|
+
}
|
|
1770
|
+
let body;
|
|
1771
|
+
try {
|
|
1772
|
+
body = JSON.parse(result.body.trim());
|
|
1773
|
+
} catch {
|
|
1774
|
+
throw readError("getDiff", "changed files");
|
|
1775
|
+
}
|
|
1776
|
+
const files = input.commit === void 0 ? body : body?.files ?? [];
|
|
1777
|
+
const decoded = decodeFiles(files);
|
|
1778
|
+
if (decoded === null) throw readError("getDiff", "changed files");
|
|
1779
|
+
const morePages = decoded.rawCount >= DIFF_FILES_PAGE_SIZE;
|
|
1780
|
+
return {
|
|
1781
|
+
patch: decoded.patch,
|
|
1782
|
+
truncated: decoded.truncated,
|
|
1783
|
+
nextCursor: morePages ? String(input.page + 1) : null,
|
|
1784
|
+
...decoded.omittedFileStats.length === 0 ? {} : { omittedFileStats: decoded.omittedFileStats }
|
|
1785
|
+
};
|
|
1786
|
+
};
|
|
1787
|
+
var getDiff = async (ctx, input) => {
|
|
1788
|
+
if (input.commit !== void 0 && !isCommitSha(input.commit)) {
|
|
1789
|
+
throw failed("getDiff", "The named commit was not a commit sha.");
|
|
1790
|
+
}
|
|
1791
|
+
const target = {
|
|
1792
|
+
cwd: input.cwd,
|
|
1793
|
+
repository: input.repository,
|
|
1794
|
+
host: input.host,
|
|
1795
|
+
number: input.number,
|
|
1796
|
+
...input.commit === void 0 ? {} : { commit: input.commit }
|
|
1797
|
+
};
|
|
1798
|
+
if (input.cursor !== void 0) {
|
|
1799
|
+
const page = diffCursorPage(input.cursor);
|
|
1800
|
+
if (page === null) {
|
|
1801
|
+
throw failed("getDiff", "The diff cursor was not one this pull request handed out.");
|
|
1802
|
+
}
|
|
1803
|
+
return diffFilesPage(ctx, { ...target, page });
|
|
1804
|
+
}
|
|
1805
|
+
if (input.commit !== void 0) return diffFilesPage(ctx, { ...target, page: 1 });
|
|
1806
|
+
const whole = async () => {
|
|
1807
|
+
const result = await callGitHubApi(ctx, {
|
|
1808
|
+
operation: "getDiff",
|
|
1809
|
+
host: input.host,
|
|
1810
|
+
path: `${repositoryPath(input, "getDiff")}/pulls/${input.number}`,
|
|
1811
|
+
accept: "application/vnd.github.diff",
|
|
1812
|
+
maxBytes: DIFF_MAX_BYTES,
|
|
1813
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1814
|
+
});
|
|
1815
|
+
return result.truncated ? null : { patch: result.body, truncated: false, nextCursor: null };
|
|
1816
|
+
};
|
|
1817
|
+
try {
|
|
1818
|
+
const slice = await whole();
|
|
1819
|
+
return slice ?? await diffFilesPage(ctx, { ...target, page: 1 });
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (error instanceof GitHubError && error.code !== "failed") throw error;
|
|
1822
|
+
try {
|
|
1823
|
+
return await diffFilesPage(ctx, { ...target, page: 1 });
|
|
1824
|
+
} catch {
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
var getDiffFileContents = async (ctx, input) => {
|
|
1830
|
+
const operation = "getDiffFileContents";
|
|
1831
|
+
if (input.commit !== void 0 && !isCommitSha(input.commit)) {
|
|
1832
|
+
throw failed(operation, "The named commit was not a commit sha.");
|
|
1833
|
+
}
|
|
1834
|
+
const repository = repositoryPath(input, operation);
|
|
1835
|
+
const revisions = decodeRevisions(
|
|
1836
|
+
await callGitHubJson(ctx, {
|
|
1837
|
+
operation,
|
|
1838
|
+
host: input.host,
|
|
1839
|
+
path: input.commit === void 0 ? `${repository}/pulls/${input.number}` : `${repository}/commits/${input.commit}`,
|
|
1840
|
+
maxBytes: DIFF_MAX_BYTES,
|
|
1841
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1842
|
+
}),
|
|
1843
|
+
input.commit === void 0 ? "pull-request" : "commit"
|
|
1844
|
+
);
|
|
1845
|
+
const rootCommitNewFile = input.commit !== void 0 && input.changeType === "new" && revisions?.baseRef === null;
|
|
1846
|
+
const baseRef = revisions?.baseRef ?? null;
|
|
1847
|
+
const headRef = revisions?.headRef ?? null;
|
|
1848
|
+
if (headRef === null || !isCommitSha(headRef) || !rootCommitNewFile && (baseRef === null || !isCommitSha(baseRef))) {
|
|
1849
|
+
throw failed(
|
|
1850
|
+
operation,
|
|
1851
|
+
input.commit === void 0 ? `Pull request #${input.number} reported no usable base and head revisions.` : `Commit ${input.commit} reported no usable revisions for this file.`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const readFile = async (revision, filePath) => {
|
|
1855
|
+
const result = await callGitHubApi(ctx, {
|
|
1856
|
+
operation,
|
|
1857
|
+
host: input.host,
|
|
1858
|
+
path: `${repository}/contents/${path(filePath)}?ref=${encodeURIComponent(revision)}`,
|
|
1859
|
+
accept: "application/vnd.github.raw",
|
|
1860
|
+
maxBytes: DIFF_FILE_MAX_BYTES,
|
|
1861
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1862
|
+
});
|
|
1863
|
+
if (result.truncated) {
|
|
1864
|
+
throw failed(operation, `The diff file '${filePath}' exceeds the 1 MB expansion limit.`);
|
|
1865
|
+
}
|
|
1866
|
+
if (result.body.includes("\0")) {
|
|
1867
|
+
throw failed(operation, `The diff file '${filePath}' is binary.`);
|
|
1868
|
+
}
|
|
1869
|
+
return result.body;
|
|
1870
|
+
};
|
|
1871
|
+
const [oldContents, newContents] = await Promise.all([
|
|
1872
|
+
input.changeType === "new" ? Promise.resolve("") : readFile(baseRef, input.oldPath),
|
|
1873
|
+
input.changeType === "deleted" ? Promise.resolve("") : readFile(headRef, input.newPath)
|
|
1874
|
+
]);
|
|
1875
|
+
return { oldContents, newContents };
|
|
1876
|
+
};
|
|
1877
|
+
var listReviewerCandidates = async (ctx, input) => {
|
|
1878
|
+
const { owner, name } = selector(input, "listReviewerCandidates");
|
|
1879
|
+
const decoded = decodeReviewerCandidates(
|
|
1880
|
+
await callGitHubGraphQl(ctx, {
|
|
1881
|
+
operation: "listReviewerCandidates",
|
|
1882
|
+
host: input.host,
|
|
1883
|
+
query: reviewerCandidatesQueryDocument(await canReadOrg(ctx, input.host)),
|
|
1884
|
+
variables: { owner, name, number: input.number }
|
|
1885
|
+
})
|
|
1886
|
+
);
|
|
1887
|
+
if (decoded === null) throw readError("listReviewerCandidates", "reviewer candidates");
|
|
1888
|
+
return decoded;
|
|
1889
|
+
};
|
|
1890
|
+
var pullRequestNodeId = async (ctx, input, operation) => {
|
|
1891
|
+
const { owner, name } = selector(input, operation);
|
|
1892
|
+
const id = decodeNodeId(
|
|
1893
|
+
await callGitHubGraphQl(ctx, {
|
|
1894
|
+
operation,
|
|
1895
|
+
host: input.host,
|
|
1896
|
+
query: NODE_ID_QUERY,
|
|
1897
|
+
variables: { owner, name, number: input.number }
|
|
1898
|
+
})
|
|
1899
|
+
);
|
|
1900
|
+
if (id === null) throw readError(operation, "pull request id");
|
|
1901
|
+
return id;
|
|
1902
|
+
};
|
|
1903
|
+
var requireSubjectScope = async (ctx, input, operation) => {
|
|
1904
|
+
const { owner, name } = selector(input, operation);
|
|
1905
|
+
const belongs = decodeSubjectScope(
|
|
1906
|
+
await callGitHubGraphQl(ctx, {
|
|
1907
|
+
operation,
|
|
1908
|
+
host: input.host,
|
|
1909
|
+
query: SUBJECT_SCOPE_QUERY,
|
|
1910
|
+
variables: { owner, name, number: input.number, subjectId: input.subjectId }
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
1913
|
+
if (!belongs) {
|
|
1914
|
+
throw failed(operation, "The named subject did not belong to the named pull request.");
|
|
1915
|
+
}
|
|
1916
|
+
return input.subjectId;
|
|
1917
|
+
};
|
|
1918
|
+
var setReviewerRequest = async (ctx, input) => {
|
|
1919
|
+
await callGitHubApi(ctx, {
|
|
1920
|
+
operation: "setReviewerRequest",
|
|
1921
|
+
host: input.host,
|
|
1922
|
+
path: `${repositoryPath(input, "setReviewerRequest")}/pulls/${input.number}/requested_reviewers`,
|
|
1923
|
+
method: input.requested ? "POST" : "DELETE",
|
|
1924
|
+
body: JSON.stringify({
|
|
1925
|
+
reviewers: input.reviewers.flatMap(
|
|
1926
|
+
(reviewer) => reviewer.kind === "user" ? [reviewer.id] : []
|
|
1927
|
+
),
|
|
1928
|
+
team_reviewers: input.reviewers.flatMap(
|
|
1929
|
+
(reviewer) => reviewer.kind === "team" ? [reviewer.id] : []
|
|
1930
|
+
)
|
|
1931
|
+
})
|
|
1932
|
+
});
|
|
1933
|
+
return null;
|
|
1934
|
+
};
|
|
1935
|
+
var runAction = async (ctx, input) => {
|
|
1936
|
+
const repository = repositoryPath(input, "runAction");
|
|
1937
|
+
const mutate = async (query2, variables) => {
|
|
1938
|
+
const pullRequestId = await pullRequestNodeId(ctx, input, "runAction");
|
|
1939
|
+
await callGitHubGraphQl(ctx, {
|
|
1940
|
+
operation: "runAction",
|
|
1941
|
+
host: input.host,
|
|
1942
|
+
query: query2,
|
|
1943
|
+
variables: { pullRequestId, ...variables }
|
|
1944
|
+
});
|
|
1945
|
+
};
|
|
1946
|
+
switch (input.action) {
|
|
1947
|
+
case "merge":
|
|
1948
|
+
await callGitHubApi(ctx, {
|
|
1949
|
+
operation: "runAction",
|
|
1950
|
+
host: input.host,
|
|
1951
|
+
path: `${repository}/pulls/${input.number}/merge`,
|
|
1952
|
+
method: "PUT",
|
|
1953
|
+
body: JSON.stringify({ merge_method: input.mergeMethod ?? "merge" })
|
|
1954
|
+
});
|
|
1955
|
+
return null;
|
|
1956
|
+
// 布置自动合并仍然要带策略:GitHub 把策略跟那条待办指令存在一起,而不是到合并那一刻
|
|
1957
|
+
// 再挑一个。
|
|
1958
|
+
case "enable-auto-merge":
|
|
1959
|
+
await mutate(ENABLE_AUTO_MERGE_MUTATION, {
|
|
1960
|
+
mergeMethod: (input.mergeMethod ?? "merge").toUpperCase()
|
|
1961
|
+
});
|
|
1962
|
+
return null;
|
|
1963
|
+
case "disable-auto-merge":
|
|
1964
|
+
await mutate(DISABLE_AUTO_MERGE_MUTATION, {});
|
|
1965
|
+
return null;
|
|
1966
|
+
// REST 的 update-branch 端点只会用合并提交更新,rebase 那一半只有 GraphQL 有。
|
|
1967
|
+
case "update-branch":
|
|
1968
|
+
await mutate(UPDATE_BRANCH_MUTATION, {
|
|
1969
|
+
updateMethod: input.updateMethod === "rebase" ? "REBASE" : "MERGE"
|
|
1970
|
+
});
|
|
1971
|
+
return null;
|
|
1972
|
+
case "ready":
|
|
1973
|
+
await mutate(READY_MUTATION, {});
|
|
1974
|
+
return null;
|
|
1975
|
+
case "draft":
|
|
1976
|
+
await mutate(DRAFT_MUTATION, {});
|
|
1977
|
+
return null;
|
|
1978
|
+
case "close":
|
|
1979
|
+
case "reopen":
|
|
1980
|
+
await callGitHubApi(ctx, {
|
|
1981
|
+
operation: "runAction",
|
|
1982
|
+
host: input.host,
|
|
1983
|
+
path: `${repository}/pulls/${input.number}`,
|
|
1984
|
+
method: "PATCH",
|
|
1985
|
+
body: JSON.stringify({ state: input.action === "close" ? "closed" : "open" })
|
|
1986
|
+
});
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
var updateChangeRequest = async (ctx, input) => {
|
|
1991
|
+
await callGitHubApi(ctx, {
|
|
1992
|
+
operation: "updateChangeRequest",
|
|
1993
|
+
host: input.host,
|
|
1994
|
+
path: `${repositoryPath(input, "updateChangeRequest")}/pulls/${input.number}`,
|
|
1995
|
+
method: "PATCH",
|
|
1996
|
+
// 调用方没点名的字段整个不发,GitHub 就把已有的字留着,而不是被要求写一个空的。
|
|
1997
|
+
body: JSON.stringify({
|
|
1998
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
1999
|
+
...input.body === void 0 ? {} : { body: input.body }
|
|
2000
|
+
})
|
|
2001
|
+
});
|
|
2002
|
+
return null;
|
|
2003
|
+
};
|
|
2004
|
+
var comment = async (ctx, input) => {
|
|
2005
|
+
await callGitHubApi(ctx, {
|
|
2006
|
+
operation: "comment",
|
|
2007
|
+
host: input.host,
|
|
2008
|
+
path: `${repositoryPath(input, "comment")}/issues/${input.number}/comments`,
|
|
2009
|
+
method: "POST",
|
|
2010
|
+
body: JSON.stringify({ body: input.body })
|
|
2011
|
+
});
|
|
2012
|
+
return null;
|
|
2013
|
+
};
|
|
2014
|
+
var updateComment = async (ctx, input) => {
|
|
2015
|
+
const commentId = await requireSubjectScope(
|
|
2016
|
+
ctx,
|
|
2017
|
+
{ ...input, subjectId: input.commentId },
|
|
2018
|
+
"updateComment"
|
|
2019
|
+
);
|
|
2020
|
+
await callGitHubGraphQl(ctx, {
|
|
2021
|
+
operation: "updateComment",
|
|
2022
|
+
host: input.host,
|
|
2023
|
+
query: input.kind === "review-comment" ? UPDATE_REVIEW_COMMENT_MUTATION : UPDATE_ISSUE_COMMENT_MUTATION,
|
|
2024
|
+
variables: { commentId, body: input.body }
|
|
2025
|
+
});
|
|
2026
|
+
return null;
|
|
2027
|
+
};
|
|
2028
|
+
var submitReview = async (ctx, input) => {
|
|
2029
|
+
await callGitHubApi(ctx, {
|
|
2030
|
+
operation: "submitReview",
|
|
2031
|
+
host: input.host,
|
|
2032
|
+
path: `${repositoryPath(input, "submitReview")}/pulls/${input.number}/reviews`,
|
|
2033
|
+
method: "POST",
|
|
2034
|
+
body: JSON.stringify({
|
|
2035
|
+
event: REVIEW_EVENT_BY_VERDICT[input.verdict],
|
|
2036
|
+
body: input.body,
|
|
2037
|
+
comments: input.comments.map((draft) => ({
|
|
2038
|
+
path: draft.path,
|
|
2039
|
+
...gitHubReviewPosition(draft.position),
|
|
2040
|
+
body: draft.body
|
|
2041
|
+
}))
|
|
2042
|
+
})
|
|
2043
|
+
});
|
|
2044
|
+
return null;
|
|
2045
|
+
};
|
|
2046
|
+
var replyToThread = async (ctx, input) => {
|
|
2047
|
+
await callGitHubGraphQl(ctx, {
|
|
2048
|
+
operation: "replyToThread",
|
|
2049
|
+
host: input.host,
|
|
2050
|
+
query: REPLY_MUTATION,
|
|
2051
|
+
variables: { threadId: input.threadId, body: input.body }
|
|
2052
|
+
});
|
|
2053
|
+
return null;
|
|
2054
|
+
};
|
|
2055
|
+
var setThreadResolution = async (ctx, input) => {
|
|
2056
|
+
await callGitHubGraphQl(ctx, {
|
|
2057
|
+
operation: "setThreadResolution",
|
|
2058
|
+
host: input.host,
|
|
2059
|
+
query: input.resolved ? RESOLVE_MUTATION : UNRESOLVE_MUTATION,
|
|
2060
|
+
variables: { threadId: input.threadId }
|
|
2061
|
+
});
|
|
2062
|
+
return null;
|
|
2063
|
+
};
|
|
2064
|
+
var setReaction = async (ctx, input) => {
|
|
2065
|
+
const subjectId = input.subjectId === void 0 ? await pullRequestNodeId(ctx, input, "setReaction") : await requireSubjectScope(ctx, { ...input, subjectId: input.subjectId }, "setReaction");
|
|
2066
|
+
await callGitHubGraphQl(ctx, {
|
|
2067
|
+
operation: "setReaction",
|
|
2068
|
+
host: input.host,
|
|
2069
|
+
query: input.reacted ? ADD_REACTION_MUTATION : REMOVE_REACTION_MUTATION,
|
|
2070
|
+
variables: { subjectId, content: gitHubReactionContent(input.content) }
|
|
2071
|
+
});
|
|
2072
|
+
return null;
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
// src/gitlabHttp.ts
|
|
2076
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
2077
|
+
var DEFAULT_MAX_BYTES2 = 1024 * 1024;
|
|
2078
|
+
var DEFAULT_HOST2 = "gitlab.com";
|
|
2079
|
+
var CARD2 = "Settings \u2192 Plugins \u2192 Pull requests";
|
|
2080
|
+
var GitLabError = class extends Error {
|
|
2081
|
+
code;
|
|
2082
|
+
constructor(input) {
|
|
2083
|
+
super(`GitLab API failed in ${input.operation}: ${input.detail}`);
|
|
2084
|
+
this.name = "GitLabError";
|
|
2085
|
+
this.code = input.code;
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
var text3 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
2089
|
+
var normalizeHost2 = (value) => value.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "").toLowerCase();
|
|
2090
|
+
var configuredHost2 = (ctx) => normalizeHost2(text3(ctx.config["gitlabHost"]) ?? DEFAULT_HOST2);
|
|
2091
|
+
var configuredToken2 = (ctx) => text3(ctx.config["gitlabToken"]);
|
|
2092
|
+
var readBody2 = async (response, maxBytes) => {
|
|
2093
|
+
const stream = response.body;
|
|
2094
|
+
if (stream === null) return { body: "", truncated: false };
|
|
2095
|
+
const reader = stream.getReader();
|
|
2096
|
+
const decoder = new TextDecoder();
|
|
2097
|
+
let body = "";
|
|
2098
|
+
let read = 0;
|
|
2099
|
+
for (; ; ) {
|
|
2100
|
+
const chunk = await reader.read();
|
|
2101
|
+
if (chunk.done) break;
|
|
2102
|
+
const value = chunk.value;
|
|
2103
|
+
if (value === void 0) continue;
|
|
2104
|
+
if (read + value.byteLength > maxBytes) {
|
|
2105
|
+
body += decoder.decode(value.subarray(0, maxBytes - read), { stream: true });
|
|
2106
|
+
await reader.cancel();
|
|
2107
|
+
return { body, truncated: true };
|
|
2108
|
+
}
|
|
2109
|
+
read += value.byteLength;
|
|
2110
|
+
body += decoder.decode(value, { stream: true });
|
|
2111
|
+
}
|
|
2112
|
+
return { body: body + decoder.decode(), truncated: false };
|
|
2113
|
+
};
|
|
2114
|
+
var statusError2 = (input) => {
|
|
2115
|
+
if (input.status === 401 || input.status === 403) {
|
|
2116
|
+
return new GitLabError({
|
|
2117
|
+
code: "unauthenticated",
|
|
2118
|
+
operation: input.operation,
|
|
2119
|
+
detail: `GitLab refused the token (HTTP ${input.status}). Check the token and its \`api\` scope in ${CARD2}.`
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
if (input.status === 429) {
|
|
2123
|
+
return new GitLabError({
|
|
2124
|
+
code: "rate-limited",
|
|
2125
|
+
operation: input.operation,
|
|
2126
|
+
detail: input.retryAfter === null ? "GitLab API rate limit exceeded." : `GitLab API rate limit exceeded; retry after ${input.retryAfter}s.`
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
return new GitLabError({
|
|
2130
|
+
code: "failed",
|
|
2131
|
+
operation: input.operation,
|
|
2132
|
+
detail: `GitLab answered HTTP ${input.status}.`
|
|
2133
|
+
});
|
|
2134
|
+
};
|
|
2135
|
+
var requestError2 = (operation, error) => {
|
|
2136
|
+
const name = error instanceof Error ? error.name : "";
|
|
2137
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
2138
|
+
return new GitLabError({
|
|
2139
|
+
code: "failed",
|
|
2140
|
+
operation,
|
|
2141
|
+
detail: "GitLab did not answer before the timeout."
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
return new GitLabError({
|
|
2145
|
+
code: "failed",
|
|
2146
|
+
operation,
|
|
2147
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2148
|
+
});
|
|
2149
|
+
};
|
|
2150
|
+
var send2 = async (ctx, input) => {
|
|
2151
|
+
const host = configuredHost2(ctx);
|
|
2152
|
+
const asked = input.host === void 0 ? null : normalizeHost2(input.host);
|
|
2153
|
+
if (asked !== null && asked.length > 0 && asked !== host) {
|
|
2154
|
+
throw new GitLabError({
|
|
2155
|
+
code: "unauthenticated",
|
|
2156
|
+
operation: input.operation,
|
|
2157
|
+
detail: `This repository lives on ${asked}, but ${CARD2} is configured for ${host}.`
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
const token = configuredToken2(ctx);
|
|
2161
|
+
if (token === null) {
|
|
2162
|
+
throw new GitLabError({
|
|
2163
|
+
code: "missing-tool",
|
|
2164
|
+
operation: input.operation,
|
|
2165
|
+
detail: `No GitLab token is configured. Add a personal access token with the \`api\` scope in ${CARD2}.`
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
let response;
|
|
2169
|
+
try {
|
|
2170
|
+
response = await ctx.http(input.url(`https://${host}`), {
|
|
2171
|
+
method: input.method ?? "GET",
|
|
2172
|
+
headers: { "PRIVATE-TOKEN": token, "Content-Type": "application/json" },
|
|
2173
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
2174
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
2175
|
+
});
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
throw requestError2(input.operation, error);
|
|
2178
|
+
}
|
|
2179
|
+
if (!response.ok) {
|
|
2180
|
+
await response.body?.cancel();
|
|
2181
|
+
throw statusError2({
|
|
2182
|
+
operation: input.operation,
|
|
2183
|
+
status: response.status,
|
|
2184
|
+
retryAfter: response.headers.get("Retry-After")
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
return await readBody2(response, input.maxBytes ?? DEFAULT_MAX_BYTES2);
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
throw requestError2(input.operation, error);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
var callGitLabApi = (ctx, input) => send2(ctx, { ...input, url: (base) => `${base}/api/v4/${input.path}` });
|
|
2194
|
+
var callGitLabGraphQl = (ctx, input) => send2(ctx, { ...input, url: (base) => `${base}/api/graphql`, method: "POST" });
|
|
2195
|
+
var readError2 = (operation, what) => new GitLabError({
|
|
2196
|
+
code: "failed",
|
|
2197
|
+
operation,
|
|
2198
|
+
detail: `GitLab returned an unreadable ${what} response.`
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
// src/gitlabMapping.ts
|
|
2202
|
+
var asObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2203
|
+
var asArray2 = (value) => Array.isArray(value) ? value : null;
|
|
2204
|
+
var text4 = (value) => {
|
|
2205
|
+
if (typeof value !== "string") return null;
|
|
2206
|
+
const trimmed = value.trim();
|
|
2207
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2208
|
+
};
|
|
2209
|
+
var rawText = (value) => typeof value === "string" ? value : null;
|
|
2210
|
+
var int2 = (value) => typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
2211
|
+
var bool = (value) => typeof value === "boolean" ? value : null;
|
|
2212
|
+
var parseJson = (raw) => {
|
|
2213
|
+
try {
|
|
2214
|
+
return JSON.parse(raw);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return void 0;
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
var toActor2 = (value) => {
|
|
2220
|
+
const raw = asObject3(value);
|
|
2221
|
+
const login = text4(raw?.username);
|
|
2222
|
+
return login === null ? null : { login, name: text4(raw?.name), avatarUrl: text4(raw?.avatar_url) };
|
|
2223
|
+
};
|
|
2224
|
+
var toLabels2 = (value) => (asArray2(value) ?? []).flatMap((entry) => {
|
|
2225
|
+
const name = text4(entry);
|
|
2226
|
+
return name === null ? [] : [{ name, color: null }];
|
|
2227
|
+
});
|
|
2228
|
+
var toPipelineStatus = (value) => {
|
|
2229
|
+
switch (text4(value)?.toLowerCase()) {
|
|
2230
|
+
case "success":
|
|
2231
|
+
return "success";
|
|
2232
|
+
case "failed":
|
|
2233
|
+
return "failure";
|
|
2234
|
+
case "canceled":
|
|
2235
|
+
case "cancelling":
|
|
2236
|
+
return "cancelled";
|
|
2237
|
+
case "skipped":
|
|
2238
|
+
return "skipped";
|
|
2239
|
+
// 等人点一下的流水线既不是进度也不是失败。
|
|
2240
|
+
case "manual":
|
|
2241
|
+
case "scheduled":
|
|
2242
|
+
return "neutral";
|
|
2243
|
+
default:
|
|
2244
|
+
return "pending";
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
var toChecks2 = (raw) => {
|
|
2248
|
+
const pipeline = asObject3(raw.head_pipeline);
|
|
2249
|
+
if (pipeline === null) return [];
|
|
2250
|
+
return [
|
|
2251
|
+
{
|
|
2252
|
+
name: "Pipeline",
|
|
2253
|
+
status: toPipelineStatus(pipeline.status),
|
|
2254
|
+
description: text4(pipeline.source),
|
|
2255
|
+
url: text4(pipeline.web_url)
|
|
2256
|
+
}
|
|
2257
|
+
];
|
|
2258
|
+
};
|
|
2259
|
+
var toState2 = (raw) => {
|
|
2260
|
+
if (text4(raw.merged_at) !== null) return "merged";
|
|
2261
|
+
switch (text4(raw.state)?.toLowerCase()) {
|
|
2262
|
+
case "merged":
|
|
2263
|
+
return "merged";
|
|
2264
|
+
case "closed":
|
|
2265
|
+
return "closed";
|
|
2266
|
+
default:
|
|
2267
|
+
return "open";
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
var toMergeability2 = (raw) => {
|
|
2271
|
+
if (bool(raw.has_conflicts) === true) return "conflicting";
|
|
2272
|
+
switch (text4(raw.merge_status)?.toLowerCase()) {
|
|
2273
|
+
case "can_be_merged":
|
|
2274
|
+
return "mergeable";
|
|
2275
|
+
case "cannot_be_merged":
|
|
2276
|
+
return "conflicting";
|
|
2277
|
+
default:
|
|
2278
|
+
return "unknown";
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
var toChangedFiles = (value) => {
|
|
2282
|
+
const parsed = Number.parseInt(text4(value) ?? "", 10);
|
|
2283
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
2284
|
+
};
|
|
2285
|
+
var toListItem = (value) => {
|
|
2286
|
+
const raw = asObject3(value);
|
|
2287
|
+
if (raw === null) return null;
|
|
2288
|
+
const number2 = int2(raw.iid);
|
|
2289
|
+
const title = rawText(raw.title);
|
|
2290
|
+
const url = rawText(raw.web_url);
|
|
2291
|
+
const headBranch = rawText(raw.source_branch);
|
|
2292
|
+
const baseBranch = rawText(raw.target_branch);
|
|
2293
|
+
const createdAt = rawText(raw.created_at);
|
|
2294
|
+
const updatedAt = rawText(raw.updated_at);
|
|
2295
|
+
if (number2 === null || title === null || url === null || headBranch === null || baseBranch === null || createdAt === null || updatedAt === null) {
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
number: number2,
|
|
2300
|
+
title,
|
|
2301
|
+
url,
|
|
2302
|
+
author: toActor2(raw.author),
|
|
2303
|
+
headBranch,
|
|
2304
|
+
baseBranch,
|
|
2305
|
+
state: toState2(raw),
|
|
2306
|
+
isDraft: bool(raw.draft) ?? bool(raw.work_in_progress) ?? false,
|
|
2307
|
+
mergeability: toMergeability2(raw),
|
|
2308
|
+
additions: 0,
|
|
2309
|
+
deletions: 0,
|
|
2310
|
+
createdAt,
|
|
2311
|
+
updatedAt,
|
|
2312
|
+
reviewRequestLogins: (asArray2(raw.reviewers) ?? []).flatMap((reviewer) => {
|
|
2313
|
+
const login = text4(asObject3(reviewer)?.username);
|
|
2314
|
+
return login === null ? [] : [login];
|
|
2315
|
+
}),
|
|
2316
|
+
labels: toLabels2(raw.labels)
|
|
2317
|
+
};
|
|
2318
|
+
};
|
|
2319
|
+
var decodeMergeRequestDetail = (raw) => {
|
|
2320
|
+
const parsed = asObject3(parseJson(raw));
|
|
2321
|
+
if (parsed === null) return null;
|
|
2322
|
+
const listItem = toListItem(parsed);
|
|
2323
|
+
if (listItem === null) return null;
|
|
2324
|
+
const pipelineAutoMerge = bool(parsed.merge_when_pipeline_succeeds);
|
|
2325
|
+
const namedAutoMerge = bool(parsed.auto_merge_enabled);
|
|
2326
|
+
const autoMerge = pipelineAutoMerge === null && namedAutoMerge === null ? void 0 : pipelineAutoMerge === true || namedAutoMerge === true;
|
|
2327
|
+
const diverged = int2(parsed.diverged_commits_count);
|
|
2328
|
+
const reviewers = asArray2(parsed.reviewers) ?? [];
|
|
2329
|
+
return {
|
|
2330
|
+
...listItem,
|
|
2331
|
+
body: rawText(parsed.description) ?? "",
|
|
2332
|
+
changedFiles: toChangedFiles(parsed.changes_count),
|
|
2333
|
+
mergedAt: text4(parsed.merged_at),
|
|
2334
|
+
closedAt: text4(parsed.closed_at),
|
|
2335
|
+
// 从 reviewer 对象本身建,头像才不会丢。
|
|
2336
|
+
reviewers: reviewers.flatMap((reviewer) => {
|
|
2337
|
+
const actor = toActor2(reviewer);
|
|
2338
|
+
return actor === null ? [] : [actor];
|
|
2339
|
+
}),
|
|
2340
|
+
checks: toChecks2(parsed),
|
|
2341
|
+
viewerCanMerge: bool(asObject3(parsed.user)?.can_merge) !== false,
|
|
2342
|
+
reviewerIds: reviewers.flatMap((reviewer) => {
|
|
2343
|
+
const id = int2(asObject3(reviewer)?.id);
|
|
2344
|
+
return id === null ? [] : [id];
|
|
2345
|
+
}),
|
|
2346
|
+
...autoMerge === void 0 ? {} : { autoMergeEnabled: autoMerge },
|
|
2347
|
+
...diverged === null ? {} : { divergedCommits: diverged }
|
|
2348
|
+
};
|
|
2349
|
+
};
|
|
2350
|
+
var decodeMergeRequestList = (raw) => {
|
|
2351
|
+
const entries = asArray2(parseJson(raw));
|
|
2352
|
+
if (entries === null) return null;
|
|
2353
|
+
const items = [];
|
|
2354
|
+
const rawIndexes = [];
|
|
2355
|
+
for (const [rawIndex, entry] of entries.entries()) {
|
|
2356
|
+
const item = toListItem(entry);
|
|
2357
|
+
if (item === null) continue;
|
|
2358
|
+
items.push(item);
|
|
2359
|
+
rawIndexes.push(rawIndex);
|
|
2360
|
+
}
|
|
2361
|
+
return { items, rawIndexes, rawCount: entries.length };
|
|
2362
|
+
};
|
|
2363
|
+
var decodeViewer = (raw) => {
|
|
2364
|
+
const parsed = asObject3(parseJson(raw));
|
|
2365
|
+
return parsed === null ? null : { username: text4(parsed.username) };
|
|
2366
|
+
};
|
|
2367
|
+
var decodeProjectUsers = (raw) => {
|
|
2368
|
+
const entries = asArray2(parseJson(raw));
|
|
2369
|
+
if (entries === null) return null;
|
|
2370
|
+
const candidates = [];
|
|
2371
|
+
for (const entry of entries) {
|
|
2372
|
+
const id = int2(asObject3(entry)?.id);
|
|
2373
|
+
if (id === null) continue;
|
|
2374
|
+
const actor = toActor2(entry);
|
|
2375
|
+
if (actor === null) continue;
|
|
2376
|
+
candidates.push({ ...actor, id: String(id), kind: "user", isRequested: false });
|
|
2377
|
+
}
|
|
2378
|
+
return { candidates, rawCount: entries.length };
|
|
2379
|
+
};
|
|
2380
|
+
var decodeProjectMergeCapabilities = (raw) => {
|
|
2381
|
+
const parsed = asObject3(parseJson(raw));
|
|
2382
|
+
if (parsed === null) return null;
|
|
2383
|
+
const mergeMethod = text4(parsed.merge_method)?.toLowerCase();
|
|
2384
|
+
const squashOption = text4(parsed.squash_option)?.toLowerCase();
|
|
2385
|
+
return {
|
|
2386
|
+
merge: mergeMethod === "merge",
|
|
2387
|
+
// 半线性与 fast-forward 两种历史都是把分支 rebase 到目标上得到的。
|
|
2388
|
+
rebase: mergeMethod === "rebase_merge" || mergeMethod === "ff",
|
|
2389
|
+
squash: squashOption === "always" || squashOption === "default_on" || squashOption === "default_off"
|
|
2390
|
+
};
|
|
2391
|
+
};
|
|
2392
|
+
var decodeNotes = (raw) => {
|
|
2393
|
+
const entries = asArray2(parseJson(raw));
|
|
2394
|
+
if (entries === null) return null;
|
|
2395
|
+
const comments = [];
|
|
2396
|
+
for (const entry of entries) {
|
|
2397
|
+
const note = asObject3(entry);
|
|
2398
|
+
const id = int2(note?.id);
|
|
2399
|
+
const createdAt = rawText(note?.created_at);
|
|
2400
|
+
if (note === null || id === null || createdAt === null) continue;
|
|
2401
|
+
if (bool(note.system) === true) continue;
|
|
2402
|
+
const body = rawText(note.body) ?? "";
|
|
2403
|
+
if (body.trim().length === 0) continue;
|
|
2404
|
+
const position = asObject3(note.position);
|
|
2405
|
+
comments.push({
|
|
2406
|
+
id: String(id),
|
|
2407
|
+
kind: text4(note.type) === "DiffNote" ? "review-comment" : "issue-comment",
|
|
2408
|
+
author: toActor2(note.author),
|
|
2409
|
+
body,
|
|
2410
|
+
createdAt,
|
|
2411
|
+
url: null,
|
|
2412
|
+
path: text4(position?.new_path) ?? text4(position?.old_path),
|
|
2413
|
+
reviewState: null
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
return { comments, rawCount: entries.length };
|
|
2417
|
+
};
|
|
2418
|
+
var decodeDiscussions = (raw) => {
|
|
2419
|
+
const entries = asArray2(parseJson(raw));
|
|
2420
|
+
if (entries === null) return null;
|
|
2421
|
+
const threads = [];
|
|
2422
|
+
for (const entry of entries) {
|
|
2423
|
+
const discussion = asObject3(entry);
|
|
2424
|
+
const id = text4(discussion?.id);
|
|
2425
|
+
if (discussion === null || id === null) continue;
|
|
2426
|
+
const notes = (asArray2(discussion.notes) ?? []).flatMap((value) => {
|
|
2427
|
+
const note = asObject3(value);
|
|
2428
|
+
if (note === null || bool(note.system) === true) return [];
|
|
2429
|
+
const noteId = int2(note.id);
|
|
2430
|
+
const createdAt = rawText(note.created_at);
|
|
2431
|
+
return noteId === null || createdAt === null ? [] : [{ note, noteId, createdAt }];
|
|
2432
|
+
});
|
|
2433
|
+
const root = notes[0];
|
|
2434
|
+
const position = asObject3(root?.note.position);
|
|
2435
|
+
if (root === void 0 || position === null || text4(position.position_type) !== "text") {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const newLine = int2(position.new_line);
|
|
2439
|
+
const side = newLine === null ? "left" : "right";
|
|
2440
|
+
const path2 = text4(side === "left" ? position.old_path : position.new_path);
|
|
2441
|
+
if (path2 === null) continue;
|
|
2442
|
+
const line = side === "left" ? int2(position.old_line) : newLine;
|
|
2443
|
+
threads.push({
|
|
2444
|
+
id,
|
|
2445
|
+
path: path2,
|
|
2446
|
+
line: line !== null && line > 0 ? line : null,
|
|
2447
|
+
side,
|
|
2448
|
+
isResolved: bool(root.note.resolved) === true,
|
|
2449
|
+
// GitLab 不报「写在一条已经挪走的行上」,diff 放不下的线程由 diff 自己算,
|
|
2450
|
+
// 不在这里声称。
|
|
2451
|
+
isOutdated: false,
|
|
2452
|
+
comments: notes.map(
|
|
2453
|
+
({ note, noteId, createdAt }) => ({
|
|
2454
|
+
id: String(noteId),
|
|
2455
|
+
author: toActor2(note.author),
|
|
2456
|
+
body: rawText(note.body) ?? "",
|
|
2457
|
+
createdAt,
|
|
2458
|
+
url: null
|
|
2459
|
+
})
|
|
2460
|
+
)
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
return { threads, rawCount: entries.length };
|
|
2464
|
+
};
|
|
2465
|
+
var decodeCommits = (raw) => {
|
|
2466
|
+
const entries = asArray2(parseJson(raw));
|
|
2467
|
+
if (entries === null) return null;
|
|
2468
|
+
const commits = [];
|
|
2469
|
+
for (const entry of entries) {
|
|
2470
|
+
const commit = asObject3(entry);
|
|
2471
|
+
const oid = text4(commit?.id);
|
|
2472
|
+
if (commit === null || oid === null) continue;
|
|
2473
|
+
const committedDate = text4(commit.committed_date) ?? text4(commit.created_at);
|
|
2474
|
+
if (committedDate === null) continue;
|
|
2475
|
+
const stats = asObject3(commit.stats);
|
|
2476
|
+
const login = text4(commit.author_name) ?? text4(commit.author_email);
|
|
2477
|
+
commits.push({
|
|
2478
|
+
oid,
|
|
2479
|
+
messageHeadline: rawText(commit.title) ?? "",
|
|
2480
|
+
committedDate,
|
|
2481
|
+
...stats === null ? {} : {
|
|
2482
|
+
additions: Math.max(0, int2(stats.additions) ?? 0),
|
|
2483
|
+
deletions: Math.max(0, int2(stats.deletions) ?? 0)
|
|
2484
|
+
},
|
|
2485
|
+
authors: login === null ? [] : [{ login, name: text4(commit.author_name), avatarUrl: null }]
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
return commits.toReversed();
|
|
2489
|
+
};
|
|
2490
|
+
var decodeDiffRefs = (raw) => {
|
|
2491
|
+
const parsed = asObject3(parseJson(raw));
|
|
2492
|
+
if (parsed === null) return null;
|
|
2493
|
+
const refs = asObject3(parsed.diff_refs);
|
|
2494
|
+
if (refs === null) return { refs: null };
|
|
2495
|
+
const baseSha = rawText(refs.base_sha);
|
|
2496
|
+
const headSha = rawText(refs.head_sha);
|
|
2497
|
+
const startSha = rawText(refs.start_sha);
|
|
2498
|
+
if (baseSha === null || headSha === null || startSha === null) return null;
|
|
2499
|
+
return { refs: { baseSha, headSha, startSha } };
|
|
2500
|
+
};
|
|
2501
|
+
var decodeMergeRequestDiffs = (raw) => {
|
|
2502
|
+
const entries = asArray2(parseJson(raw));
|
|
2503
|
+
if (entries === null) return null;
|
|
2504
|
+
const sections = [];
|
|
2505
|
+
let truncated = false;
|
|
2506
|
+
for (const entry of entries) {
|
|
2507
|
+
const file = asObject3(entry);
|
|
2508
|
+
const oldPath = rawText(file?.old_path);
|
|
2509
|
+
const newPath = rawText(file?.new_path);
|
|
2510
|
+
if (file === null || oldPath === null || newPath === null) continue;
|
|
2511
|
+
const hunks = rawText(file.diff) ?? "";
|
|
2512
|
+
const isNew = bool(file.new_file) === true;
|
|
2513
|
+
const isDeleted = bool(file.deleted_file) === true;
|
|
2514
|
+
if (hunks.length === 0) {
|
|
2515
|
+
truncated = truncated || bool(file.too_large) === true || bool(file.collapsed) === true;
|
|
2516
|
+
}
|
|
2517
|
+
const header = [
|
|
2518
|
+
`diff --git a/${oldPath} b/${newPath}`,
|
|
2519
|
+
...isNew ? [`new file mode ${rawText(file.b_mode) ?? "100644"}`] : [],
|
|
2520
|
+
...isDeleted ? [`deleted file mode ${rawText(file.a_mode) ?? "100644"}`] : [],
|
|
2521
|
+
...bool(file.renamed_file) === true ? [`rename from ${oldPath}`, `rename to ${newPath}`] : [],
|
|
2522
|
+
`--- ${isNew ? "/dev/null" : `a/${oldPath}`}`,
|
|
2523
|
+
`+++ ${isDeleted ? "/dev/null" : `b/${newPath}`}`
|
|
2524
|
+
].join("\n");
|
|
2525
|
+
sections.push(hunks.length === 0 ? header : `${header}
|
|
2526
|
+
${hunks.replace(/\n?$/, "\n")}`);
|
|
2527
|
+
}
|
|
2528
|
+
return { patch: sections.join("\n"), truncated, rawCount: entries.length };
|
|
2529
|
+
};
|
|
2530
|
+
var GITLAB_AWARD_BY_CONTENT = {
|
|
2531
|
+
"thumbs-up": "thumbsup",
|
|
2532
|
+
"thumbs-down": "thumbsdown",
|
|
2533
|
+
laugh: "laughing",
|
|
2534
|
+
hooray: "tada",
|
|
2535
|
+
confused: "confused",
|
|
2536
|
+
heart: "heart",
|
|
2537
|
+
rocket: "rocket",
|
|
2538
|
+
eyes: "eyes"
|
|
2539
|
+
};
|
|
2540
|
+
var CONTENT_BY_GITLAB_AWARD = Object.fromEntries(
|
|
2541
|
+
Object.entries(GITLAB_AWARD_BY_CONTENT).map(([content, name]) => [name, content])
|
|
2542
|
+
);
|
|
2543
|
+
var gitLabAwardName = (content) => GITLAB_AWARD_BY_CONTENT[content];
|
|
2544
|
+
var AWARD_EMOJI_GRAPHQL_QUERY = `query($fullPath: ID!, $iid: String!, $cursor: String) {
|
|
2545
|
+
currentUser { username }
|
|
2546
|
+
project(fullPath: $fullPath) {
|
|
2547
|
+
mergeRequest(iid: $iid) {
|
|
2548
|
+
awardEmoji { nodes { name user { username } } }
|
|
2549
|
+
notes(first: 100, after: $cursor) {
|
|
2550
|
+
pageInfo { hasNextPage endCursor }
|
|
2551
|
+
nodes { id awardEmoji { nodes { name user { username } } } }
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}`;
|
|
2556
|
+
var toReactions2 = (value, viewer) => {
|
|
2557
|
+
const normalizedViewer = viewer?.toLowerCase() ?? null;
|
|
2558
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2559
|
+
for (const node of asArray2(asObject3(value)?.nodes) ?? []) {
|
|
2560
|
+
const content = CONTENT_BY_GITLAB_AWARD[text4(asObject3(node)?.name)?.toLowerCase() ?? ""];
|
|
2561
|
+
if (content === void 0) continue;
|
|
2562
|
+
const username = text4(asObject3(asObject3(node)?.user)?.username);
|
|
2563
|
+
if (username === null) continue;
|
|
2564
|
+
const group = groups.get(content) ?? { count: 0, actors: [], viewer: false };
|
|
2565
|
+
group.count++;
|
|
2566
|
+
if (normalizedViewer !== null && username.toLowerCase() === normalizedViewer) {
|
|
2567
|
+
group.viewer = true;
|
|
2568
|
+
} else {
|
|
2569
|
+
group.actors.push(username);
|
|
2570
|
+
}
|
|
2571
|
+
groups.set(content, group);
|
|
2572
|
+
}
|
|
2573
|
+
return [...groups].flatMap(
|
|
2574
|
+
([content, group]) => group.count === 0 ? [] : [{ content, count: group.count, actors: group.actors, viewerHasReacted: group.viewer }]
|
|
2575
|
+
);
|
|
2576
|
+
};
|
|
2577
|
+
var noteIdOf = (value) => {
|
|
2578
|
+
const id = text4(value)?.split("/").at(-1);
|
|
2579
|
+
return id !== void 0 && /^\d+$/.test(id) ? id : null;
|
|
2580
|
+
};
|
|
2581
|
+
var decodeAwardEmoji = (raw) => {
|
|
2582
|
+
const data = asObject3(asObject3(parseJson(raw))?.data);
|
|
2583
|
+
if (data === null) return null;
|
|
2584
|
+
const viewer = text4(asObject3(data.currentUser)?.username);
|
|
2585
|
+
const mergeRequest = asObject3(asObject3(data.project)?.mergeRequest);
|
|
2586
|
+
const notes = asObject3(mergeRequest?.notes);
|
|
2587
|
+
const reactionsByNoteId = /* @__PURE__ */ new Map();
|
|
2588
|
+
for (const node of asArray2(notes?.nodes) ?? []) {
|
|
2589
|
+
const id = noteIdOf(asObject3(node)?.id);
|
|
2590
|
+
if (id === null) continue;
|
|
2591
|
+
const reactions = toReactions2(asObject3(node)?.awardEmoji, viewer);
|
|
2592
|
+
if (reactions.length > 0) reactionsByNoteId.set(id, reactions);
|
|
2593
|
+
}
|
|
2594
|
+
const pageInfo = asObject3(notes?.pageInfo);
|
|
2595
|
+
return {
|
|
2596
|
+
reactions: toReactions2(mergeRequest?.awardEmoji, viewer),
|
|
2597
|
+
reactionsByNoteId,
|
|
2598
|
+
nextCursor: bool(pageInfo?.hasNextPage) === true ? text4(pageInfo?.endCursor) : null
|
|
2599
|
+
};
|
|
2600
|
+
};
|
|
2601
|
+
var decodeOwnAwardId = (raw, input) => {
|
|
2602
|
+
const entries = asArray2(parseJson(raw));
|
|
2603
|
+
if (entries === null) return null;
|
|
2604
|
+
const name = gitLabAwardName(input.content);
|
|
2605
|
+
for (const entry of entries) {
|
|
2606
|
+
const award = asObject3(entry);
|
|
2607
|
+
const id = int2(award?.id);
|
|
2608
|
+
if (award === null || id === null) continue;
|
|
2609
|
+
if (text4(award.name)?.toLowerCase() !== name) continue;
|
|
2610
|
+
if (text4(asObject3(award.user)?.username) !== input.viewer) continue;
|
|
2611
|
+
return { awardId: id };
|
|
2612
|
+
}
|
|
2613
|
+
return { awardId: null };
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2616
|
+
// src/gitlabCommands.ts
|
|
2617
|
+
var CAPABILITIES2 = {
|
|
2618
|
+
diff: true,
|
|
2619
|
+
comment: true,
|
|
2620
|
+
actions: [
|
|
2621
|
+
"merge",
|
|
2622
|
+
"ready",
|
|
2623
|
+
"draft",
|
|
2624
|
+
"close",
|
|
2625
|
+
"reopen",
|
|
2626
|
+
"update-branch",
|
|
2627
|
+
"enable-auto-merge",
|
|
2628
|
+
"disable-auto-merge"
|
|
2629
|
+
],
|
|
2630
|
+
// GitLab offers all three, though a project settles on one; `mergeCapabilities` narrows it.
|
|
2631
|
+
mergeMethods: ["merge", "squash", "rebase"],
|
|
2632
|
+
// Rebase alone: GitLab moves a stale branch onto its target by replaying it, and has nothing
|
|
2633
|
+
// that merges the target back in the way GitHub's update button can. Declaring only what it
|
|
2634
|
+
// does is what lets a request to merge the target in be refused instead of quietly rebasing.
|
|
2635
|
+
updateMethods: ["rebase"],
|
|
2636
|
+
search: true,
|
|
2637
|
+
reactions: true,
|
|
2638
|
+
review: {
|
|
2639
|
+
inlineComment: true,
|
|
2640
|
+
reply: true,
|
|
2641
|
+
resolve: true,
|
|
2642
|
+
// No "changes requested": GitLab has approval and unresolved discussions, and nothing that
|
|
2643
|
+
// says a merge request has been reviewed and rejected.
|
|
2644
|
+
verdicts: ["comment", "approve"]
|
|
2645
|
+
},
|
|
2646
|
+
reviewers: { request: true, listCandidates: true },
|
|
2647
|
+
edit: { changeRequest: true, comment: true }
|
|
2648
|
+
};
|
|
2649
|
+
var MAX_PAGE_SIZE = 100;
|
|
2650
|
+
var COMMIT_PAGE_SIZE = 100;
|
|
2651
|
+
var CONVERSATION_PAGES = 10;
|
|
2652
|
+
var DIFF_MAX_BYTES2 = 8 * 1024 * 1024;
|
|
2653
|
+
var DIFF_TIMEOUT_MS2 = 6e4;
|
|
2654
|
+
var MERGE_ACTIONS = /* @__PURE__ */ new Set([
|
|
2655
|
+
"merge",
|
|
2656
|
+
"update-branch",
|
|
2657
|
+
"enable-auto-merge",
|
|
2658
|
+
"disable-auto-merge"
|
|
2659
|
+
]);
|
|
2660
|
+
var gitLabViewerPermissions = (input) => ({
|
|
2661
|
+
// 布置自动合并与撤销布置就是延后的合并,跟着同一个 `can_merge`。
|
|
2662
|
+
actions: CAPABILITIES2.actions.filter(
|
|
2663
|
+
(action) => !MERGE_ACTIONS.has(action) || input.viewerCanMerge
|
|
2664
|
+
),
|
|
2665
|
+
comment: true,
|
|
2666
|
+
resolve: true,
|
|
2667
|
+
verdicts: CAPABILITIES2.review.verdicts,
|
|
2668
|
+
requestReviewers: true,
|
|
2669
|
+
...input.viewerCanMerge ? { updateMethods: CAPABILITIES2.updateMethods } : {}
|
|
2670
|
+
});
|
|
2671
|
+
var projectPath = (repository) => encodeURIComponent(repository.trim());
|
|
2672
|
+
var query = (params) => params.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&");
|
|
2673
|
+
var stateParam = (state) => (
|
|
2674
|
+
// GitLab 的 `closed` 本来就不含已合并的,不用再加过滤;`all` 覆盖全部状态。
|
|
2675
|
+
state === "open" ? "opened" : state
|
|
2676
|
+
);
|
|
2677
|
+
var involvementParams = (input) => {
|
|
2678
|
+
switch (input.involvement) {
|
|
2679
|
+
case "authored":
|
|
2680
|
+
return [["author_username", input.viewer]];
|
|
2681
|
+
case "reviewing":
|
|
2682
|
+
return [["reviewer_username", input.viewer]];
|
|
2683
|
+
case "all":
|
|
2684
|
+
return [];
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
var searchParams = (search) => {
|
|
2688
|
+
const trimmed = search?.trim() ?? "";
|
|
2689
|
+
return trimmed.length === 0 ? [] : [["search", trimmed]];
|
|
2690
|
+
};
|
|
2691
|
+
var diffCursorPage2 = (cursor) => /^[1-9][0-9]{0,6}$/.test(cursor) ? Number(cursor) : null;
|
|
2692
|
+
var isCommitSha2 = (value) => /^[0-9a-f]{7,64}$/i.test(value);
|
|
2693
|
+
var DRAFT_PREFIX = /^\s*(?:\[draft\]|\(draft\)|draft:|wip:)\s*/i;
|
|
2694
|
+
var reviewPositionLines = (position) => {
|
|
2695
|
+
switch (position.kind) {
|
|
2696
|
+
case "added":
|
|
2697
|
+
return { new_line: position.newLine };
|
|
2698
|
+
case "deleted":
|
|
2699
|
+
return { old_line: position.oldLine };
|
|
2700
|
+
case "context":
|
|
2701
|
+
return { old_line: position.oldLine, new_line: position.newLine };
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
var mergeRequestPath = (input) => `projects/${projectPath(input.repository)}/merge_requests/${input.number}`;
|
|
2705
|
+
var getViewer2 = async (ctx) => {
|
|
2706
|
+
const result = await callGitLabApi(ctx, {
|
|
2707
|
+
operation: "getViewer",
|
|
2708
|
+
path: "user"
|
|
2709
|
+
});
|
|
2710
|
+
const decoded = decodeViewer(result.body.trim());
|
|
2711
|
+
if (decoded === null) throw readError2("getViewer", "getViewerUsername");
|
|
2712
|
+
if (decoded.username === null) {
|
|
2713
|
+
throw new GitLabError({
|
|
2714
|
+
code: "failed",
|
|
2715
|
+
operation: "getViewer",
|
|
2716
|
+
detail: "GitLab returned no username for the authenticated account."
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
return decoded.username;
|
|
2720
|
+
};
|
|
2721
|
+
var listChangeRequests2 = async (ctx, input) => {
|
|
2722
|
+
const delivered = input.cursor?.delivered ?? 0;
|
|
2723
|
+
const perPage = Math.min(input.limit + 1, MAX_PAGE_SIZE);
|
|
2724
|
+
const firstPage = Math.floor(delivered / perPage) + 1;
|
|
2725
|
+
const lastPage = Math.floor((delivered + input.limit) / perPage) + 1;
|
|
2726
|
+
let page = firstPage;
|
|
2727
|
+
let collected = [];
|
|
2728
|
+
let cursorAdvance = 0;
|
|
2729
|
+
for (; ; ) {
|
|
2730
|
+
const skipOnFirstPage = page === firstPage ? delivered % perPage : 0;
|
|
2731
|
+
const result = await callGitLabApi(ctx, {
|
|
2732
|
+
operation: "listChangeRequests",
|
|
2733
|
+
host: input.host,
|
|
2734
|
+
path: `projects/${projectPath(input.repository)}/merge_requests?${query([
|
|
2735
|
+
["state", stateParam(input.state)],
|
|
2736
|
+
...involvementParams(input),
|
|
2737
|
+
// 搜索用 REST 自己的 `search` 参数(匹配标题与描述),并像别的值一样 URL
|
|
2738
|
+
// 编码,里面的文字变不成另一个参数。
|
|
2739
|
+
...searchParams(input.query),
|
|
2740
|
+
["order_by", "updated_at"],
|
|
2741
|
+
["sort", "desc"],
|
|
2742
|
+
["per_page", String(perPage)],
|
|
2743
|
+
["page", String(page)]
|
|
2744
|
+
])}`
|
|
2745
|
+
});
|
|
2746
|
+
const raw = result.body.trim();
|
|
2747
|
+
if (raw.length === 0) {
|
|
2748
|
+
return { items: collected, truncated: false, cursorAdvance, continues: true };
|
|
2749
|
+
}
|
|
2750
|
+
const decoded = decodeMergeRequestList(raw);
|
|
2751
|
+
if (decoded === null) throw readError2("listChangeRequests", "listMergeRequests");
|
|
2752
|
+
const pageItems = [];
|
|
2753
|
+
const pageRawIndexes = [];
|
|
2754
|
+
for (const [index, item] of decoded.items.entries()) {
|
|
2755
|
+
const rawIndex = decoded.rawIndexes[index];
|
|
2756
|
+
if (rawIndex < skipOnFirstPage) continue;
|
|
2757
|
+
pageItems.push(item);
|
|
2758
|
+
pageRawIndexes.push(rawIndex);
|
|
2759
|
+
}
|
|
2760
|
+
const remaining = input.limit - collected.length;
|
|
2761
|
+
const lastItemRawIndex = pageRawIndexes[remaining - 1];
|
|
2762
|
+
if (lastItemRawIndex !== void 0) {
|
|
2763
|
+
return {
|
|
2764
|
+
items: [...collected, ...pageItems.slice(0, remaining)],
|
|
2765
|
+
truncated: lastItemRawIndex + 1 < decoded.rawCount || decoded.rawCount === perPage,
|
|
2766
|
+
cursorAdvance: cursorAdvance + (lastItemRawIndex + 1 - skipOnFirstPage),
|
|
2767
|
+
continues: true
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
collected = [...collected, ...pageItems];
|
|
2771
|
+
cursorAdvance += Math.max(0, decoded.rawCount - skipOnFirstPage);
|
|
2772
|
+
if (decoded.rawCount < perPage) {
|
|
2773
|
+
return { items: collected, truncated: false, cursorAdvance, continues: true };
|
|
2774
|
+
}
|
|
2775
|
+
if (page >= lastPage) {
|
|
2776
|
+
return { items: collected, truncated: true, cursorAdvance, continues: true };
|
|
2777
|
+
}
|
|
2778
|
+
page += 1;
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
var mergeRequestDetail = async (ctx, input, operation) => {
|
|
2782
|
+
const result = await callGitLabApi(ctx, {
|
|
2783
|
+
operation,
|
|
2784
|
+
host: input.host,
|
|
2785
|
+
// 落后目标分支多少只有点名要才给,而且就在这一次读里要——同一条合并请求,不值得
|
|
2786
|
+
// 再发一个请求。
|
|
2787
|
+
path: `${mergeRequestPath(input)}?${query([["include_diverged_commits_count", "true"]])}`
|
|
2788
|
+
});
|
|
2789
|
+
const decoded = decodeMergeRequestDetail(result.body.trim());
|
|
2790
|
+
if (decoded === null) throw readError2(operation, "getMergeRequestDetail");
|
|
2791
|
+
return decoded;
|
|
2792
|
+
};
|
|
2793
|
+
var projectMergeCapabilities = async (ctx, input, operation) => {
|
|
2794
|
+
const result = await callGitLabApi(ctx, {
|
|
2795
|
+
operation,
|
|
2796
|
+
host: input.host,
|
|
2797
|
+
path: `projects/${projectPath(input.repository)}?license=false`
|
|
2798
|
+
});
|
|
2799
|
+
const decoded = decodeProjectMergeCapabilities(result.body.trim());
|
|
2800
|
+
if (decoded === null) throw readError2(operation, "getProjectMergeCapabilities");
|
|
2801
|
+
return decoded;
|
|
2802
|
+
};
|
|
2803
|
+
var getChangeRequest2 = async (ctx, input) => {
|
|
2804
|
+
const [mergeRequest, mergeCapabilities] = await Promise.all([
|
|
2805
|
+
mergeRequestDetail(ctx, input, "getChangeRequest"),
|
|
2806
|
+
projectMergeCapabilities(ctx, input, "getChangeRequest")
|
|
2807
|
+
]);
|
|
2808
|
+
return {
|
|
2809
|
+
...mergeRequest,
|
|
2810
|
+
mergeCapabilities,
|
|
2811
|
+
viewerPermissions: gitLabViewerPermissions(mergeRequest),
|
|
2812
|
+
// 数不出分歧数的老 GitLab 什么都不说,而不是说「已是最新」:横幅丢了不要紧,一个
|
|
2813
|
+
// 错误的「一切正常」不值得显示。
|
|
2814
|
+
baseComparison: mergeRequest.divergedCommits === void 0 ? "unknown" : mergeRequest.divergedCommits > 0 ? "behind" : "up-to-date",
|
|
2815
|
+
...mergeRequest.divergedCommits === void 0 ? {} : { behindBy: mergeRequest.divergedCommits }
|
|
2816
|
+
};
|
|
2817
|
+
};
|
|
2818
|
+
var getViewerPermissions2 = async (ctx, input) => (
|
|
2819
|
+
// 与 detail 取自同一次读:`user.can_merge` 就长在合并请求上,没有更便宜的问法。
|
|
2820
|
+
gitLabViewerPermissions(await mergeRequestDetail(ctx, input, "getViewerPermissions"))
|
|
2821
|
+
);
|
|
2822
|
+
var listNotes = async (ctx, input) => {
|
|
2823
|
+
let collected = [];
|
|
2824
|
+
for (let page = 1; ; page += 1) {
|
|
2825
|
+
const result = await callGitLabApi(ctx, {
|
|
2826
|
+
operation: "listNotes",
|
|
2827
|
+
host: input.host,
|
|
2828
|
+
path: `${mergeRequestPath(input)}/notes?${query([
|
|
2829
|
+
["per_page", String(MAX_PAGE_SIZE)],
|
|
2830
|
+
["page", String(page)],
|
|
2831
|
+
["order_by", "created_at"],
|
|
2832
|
+
["sort", "asc"]
|
|
2833
|
+
])}`
|
|
2834
|
+
});
|
|
2835
|
+
const decoded = decodeNotes(result.body.trim());
|
|
2836
|
+
if (decoded === null) throw readError2("listNotes", "listNotes");
|
|
2837
|
+
collected = [...collected, ...decoded.comments];
|
|
2838
|
+
if (decoded.rawCount < MAX_PAGE_SIZE) return { comments: collected, truncated: false };
|
|
2839
|
+
if (page >= CONVERSATION_PAGES) return { comments: collected, truncated: true };
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
var listDiscussions = async (ctx, input) => {
|
|
2843
|
+
let collected = [];
|
|
2844
|
+
for (let page = 1; ; page += 1) {
|
|
2845
|
+
const result = await callGitLabApi(ctx, {
|
|
2846
|
+
operation: "listDiscussions",
|
|
2847
|
+
host: input.host,
|
|
2848
|
+
path: `${mergeRequestPath(input)}/discussions?${query([
|
|
2849
|
+
["per_page", String(MAX_PAGE_SIZE)],
|
|
2850
|
+
["page", String(page)]
|
|
2851
|
+
])}`
|
|
2852
|
+
});
|
|
2853
|
+
const decoded = decodeDiscussions(result.body.trim());
|
|
2854
|
+
if (decoded === null) throw readError2("listDiscussions", "listDiscussions");
|
|
2855
|
+
collected = [...collected, ...decoded.threads];
|
|
2856
|
+
if (decoded.rawCount < MAX_PAGE_SIZE) return { threads: collected, truncated: false };
|
|
2857
|
+
if (page >= CONVERSATION_PAGES) return { threads: collected, truncated: true };
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
var listCommits = async (ctx, input) => {
|
|
2861
|
+
const result = await callGitLabApi(ctx, {
|
|
2862
|
+
operation: "listCommits",
|
|
2863
|
+
host: input.host,
|
|
2864
|
+
path: `${mergeRequestPath(input)}/commits?${query([
|
|
2865
|
+
["per_page", String(COMMIT_PAGE_SIZE)],
|
|
2866
|
+
["with_stats", "true"]
|
|
2867
|
+
])}`
|
|
2868
|
+
});
|
|
2869
|
+
const decoded = decodeCommits(result.body.trim());
|
|
2870
|
+
if (decoded === null) throw readError2("listCommits", "listCommits");
|
|
2871
|
+
return decoded;
|
|
2872
|
+
};
|
|
2873
|
+
var listReactions = async (ctx, input) => {
|
|
2874
|
+
let cursor = null;
|
|
2875
|
+
let reactions = [];
|
|
2876
|
+
const reactionsByNoteId = /* @__PURE__ */ new Map();
|
|
2877
|
+
for (let page = 1; ; page += 1) {
|
|
2878
|
+
const result = await callGitLabGraphQl(ctx, {
|
|
2879
|
+
operation: "listReactions",
|
|
2880
|
+
host: input.host,
|
|
2881
|
+
body: JSON.stringify({
|
|
2882
|
+
query: AWARD_EMOJI_GRAPHQL_QUERY,
|
|
2883
|
+
variables: { fullPath: input.repository, iid: String(input.number), cursor }
|
|
2884
|
+
})
|
|
2885
|
+
});
|
|
2886
|
+
const decoded = decodeAwardEmoji(result.body.trim());
|
|
2887
|
+
if (decoded === null) throw readError2("listReactions", "listReactions");
|
|
2888
|
+
if (page === 1) reactions = decoded.reactions;
|
|
2889
|
+
for (const [id, noteReactions] of decoded.reactionsByNoteId) {
|
|
2890
|
+
reactionsByNoteId.set(id, noteReactions);
|
|
2891
|
+
}
|
|
2892
|
+
if (decoded.nextCursor === null || page >= CONVERSATION_PAGES) {
|
|
2893
|
+
return { reactions, reactionsByNoteId };
|
|
2894
|
+
}
|
|
2895
|
+
cursor = decoded.nextCursor;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
var orElse = (promise, fallback) => promise.catch(() => fallback);
|
|
2899
|
+
var getChangeRequestActivity2 = async (ctx, input) => {
|
|
2900
|
+
const [notes, commits, discussions, awards] = await Promise.all([
|
|
2901
|
+
orElse(listNotes(ctx, input), {
|
|
2902
|
+
comments: [],
|
|
2903
|
+
truncated: true
|
|
2904
|
+
}),
|
|
2905
|
+
orElse(listCommits(ctx, input), []),
|
|
2906
|
+
orElse(listDiscussions(ctx, input), {
|
|
2907
|
+
threads: [],
|
|
2908
|
+
truncated: true
|
|
2909
|
+
}),
|
|
2910
|
+
// notes 端点一个表情都不带,所以跟它并排读。读挂了赔掉的是对话的表情,不是它的字。
|
|
2911
|
+
orElse(listReactions(ctx, input), {
|
|
2912
|
+
reactions: [],
|
|
2913
|
+
reactionsByNoteId: /* @__PURE__ */ new Map()
|
|
2914
|
+
})
|
|
2915
|
+
]);
|
|
2916
|
+
return {
|
|
2917
|
+
reactions: awards.reactions,
|
|
2918
|
+
comments: notes.comments.map((comment3) => ({
|
|
2919
|
+
...comment3,
|
|
2920
|
+
reactions: awards.reactionsByNoteId.get(comment3.id) ?? []
|
|
2921
|
+
})),
|
|
2922
|
+
// GitLab 不报自己的总数,所以这次走出来的总数就是主机的:notes 端点带着合并请求
|
|
2923
|
+
// 上的每一条评论,包括写在讨论下面的,一直读到 GitLab 没有为止。
|
|
2924
|
+
commentCount: notes.comments.length,
|
|
2925
|
+
commentsTruncated: notes.truncated || discussions.truncated,
|
|
2926
|
+
reviewThreads: discussions.threads.map((thread) => ({
|
|
2927
|
+
...thread,
|
|
2928
|
+
comments: thread.comments.map((comment3) => ({
|
|
2929
|
+
...comment3,
|
|
2930
|
+
reactions: awards.reactionsByNoteId.get(comment3.id) ?? []
|
|
2931
|
+
}))
|
|
2932
|
+
})),
|
|
2933
|
+
commits
|
|
2934
|
+
};
|
|
2935
|
+
};
|
|
2936
|
+
var diffPage = async (ctx, input) => {
|
|
2937
|
+
const result = await callGitLabApi(ctx, {
|
|
2938
|
+
operation: "getDiff",
|
|
2939
|
+
host: input.host,
|
|
2940
|
+
path: `projects/${projectPath(input.repository)}/${input.commit === void 0 ? `merge_requests/${input.number}/diffs` : `repository/commits/${input.commit}/diff`}?${query([
|
|
2941
|
+
["per_page", String(MAX_PAGE_SIZE)],
|
|
2942
|
+
["page", String(input.page)]
|
|
2943
|
+
])}`,
|
|
2944
|
+
maxBytes: DIFF_MAX_BYTES2,
|
|
2945
|
+
timeoutMs: DIFF_TIMEOUT_MS2
|
|
2946
|
+
});
|
|
2947
|
+
if (result.truncated) {
|
|
2948
|
+
throw new GitLabError({
|
|
2949
|
+
code: "failed",
|
|
2950
|
+
operation: "getDiff",
|
|
2951
|
+
detail: `Page ${input.page} of the merge request diff was too large to read.`
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
const decoded = decodeMergeRequestDiffs(result.body.trim());
|
|
2955
|
+
if (decoded === null) throw readError2("getDiff", "getMergeRequestDiff");
|
|
2956
|
+
const morePages = decoded.rawCount >= MAX_PAGE_SIZE;
|
|
2957
|
+
return {
|
|
2958
|
+
// 切片以换行结尾,GitLab 只给了头没给 hunk 的文件才不会撞进下一段的第一行。
|
|
2959
|
+
patch: decoded.patch.length === 0 ? decoded.patch : decoded.patch.replace(/\n?$/, "\n"),
|
|
2960
|
+
truncated: decoded.truncated,
|
|
2961
|
+
nextCursor: morePages ? String(input.page + 1) : null
|
|
2962
|
+
};
|
|
2963
|
+
};
|
|
2964
|
+
var getDiff2 = (ctx, input) => {
|
|
2965
|
+
if (input.commit !== void 0 && !isCommitSha2(input.commit)) {
|
|
2966
|
+
return Promise.reject(
|
|
2967
|
+
new GitLabError({
|
|
2968
|
+
code: "failed",
|
|
2969
|
+
operation: "getDiff",
|
|
2970
|
+
detail: "The named commit was not a commit sha."
|
|
2971
|
+
})
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
const target = {
|
|
2975
|
+
cwd: input.cwd,
|
|
2976
|
+
repository: input.repository,
|
|
2977
|
+
host: input.host,
|
|
2978
|
+
number: input.number,
|
|
2979
|
+
...input.commit === void 0 ? {} : { commit: input.commit }
|
|
2980
|
+
};
|
|
2981
|
+
if (input.cursor === void 0) return diffPage(ctx, { ...target, page: 1 });
|
|
2982
|
+
const page = diffCursorPage2(input.cursor);
|
|
2983
|
+
return page === null ? Promise.reject(
|
|
2984
|
+
new GitLabError({
|
|
2985
|
+
code: "failed",
|
|
2986
|
+
operation: "getDiff",
|
|
2987
|
+
detail: "The diff cursor was not one this merge request handed out."
|
|
2988
|
+
})
|
|
2989
|
+
) : diffPage(ctx, { ...target, page });
|
|
2990
|
+
};
|
|
2991
|
+
var projectUsers = async (ctx, input) => {
|
|
2992
|
+
const result = await callGitLabApi(ctx, {
|
|
2993
|
+
operation: "listReviewerCandidates",
|
|
2994
|
+
host: input.host,
|
|
2995
|
+
path: `projects/${projectPath(input.repository)}/users?${query([
|
|
2996
|
+
["per_page", String(MAX_PAGE_SIZE)]
|
|
2997
|
+
])}`
|
|
2998
|
+
});
|
|
2999
|
+
const decoded = decodeProjectUsers(result.body.trim());
|
|
3000
|
+
if (decoded === null) throw readError2("listReviewerCandidates", "listReviewerCandidates");
|
|
3001
|
+
return decoded;
|
|
3002
|
+
};
|
|
3003
|
+
var listReviewerCandidates2 = async (ctx, input) => {
|
|
3004
|
+
const [mergeRequest, users] = await Promise.all([
|
|
3005
|
+
mergeRequestDetail(ctx, input, "listReviewerCandidates"),
|
|
3006
|
+
projectUsers(ctx, input)
|
|
3007
|
+
]);
|
|
3008
|
+
const author = mergeRequest.author?.login;
|
|
3009
|
+
const requested = new Set(mergeRequest.reviewRequestLogins);
|
|
3010
|
+
return {
|
|
3011
|
+
// 作者被剔掉而不是显示成不能用的一项:GitLab 不让开合并请求的人当它的评审人。
|
|
3012
|
+
candidates: users.candidates.flatMap(
|
|
3013
|
+
(candidate) => candidate.login === author ? [] : [{ ...candidate, isRequested: requested.has(candidate.login) }]
|
|
3014
|
+
),
|
|
3015
|
+
truncated: users.rawCount >= MAX_PAGE_SIZE
|
|
3016
|
+
};
|
|
3017
|
+
};
|
|
3018
|
+
var setReviewerRequest2 = async (ctx, input) => {
|
|
3019
|
+
const mergeRequest = await mergeRequestDetail(ctx, input, "setReviewerRequest");
|
|
3020
|
+
const ids = new Set(mergeRequest.reviewerIds);
|
|
3021
|
+
for (const reviewer of input.reviewers) {
|
|
3022
|
+
const id = Number(reviewer.id);
|
|
3023
|
+
if (!Number.isSafeInteger(id) || id <= 0) continue;
|
|
3024
|
+
if (input.requested) ids.add(id);
|
|
3025
|
+
else ids.delete(id);
|
|
3026
|
+
}
|
|
3027
|
+
await callGitLabApi(ctx, {
|
|
3028
|
+
operation: "setReviewerRequest",
|
|
3029
|
+
host: input.host,
|
|
3030
|
+
path: mergeRequestPath(input),
|
|
3031
|
+
method: "PUT",
|
|
3032
|
+
body: JSON.stringify({ reviewer_ids: [...ids] })
|
|
3033
|
+
});
|
|
3034
|
+
return null;
|
|
3035
|
+
};
|
|
3036
|
+
var runAction2 = async (ctx, input) => {
|
|
3037
|
+
const write = (path2, method, body) => callGitLabApi(ctx, {
|
|
3038
|
+
operation: "runAction",
|
|
3039
|
+
host: input.host,
|
|
3040
|
+
path: path2,
|
|
3041
|
+
method,
|
|
3042
|
+
...body === void 0 ? {} : { body }
|
|
3043
|
+
});
|
|
3044
|
+
switch (input.action) {
|
|
3045
|
+
// 合并策略定在项目上(`mergeCapabilities` 报的就是它),端点只认 squash 这一个开关。
|
|
3046
|
+
// `merge_when_pipeline_succeeds` 显式写死两个方向:默认由 GitLab 决定的话,有流水线
|
|
3047
|
+
// 在跑时「现在就合」会静默变成「排队等」。
|
|
3048
|
+
case "merge":
|
|
3049
|
+
case "enable-auto-merge":
|
|
3050
|
+
await write(
|
|
3051
|
+
`${mergeRequestPath(input)}/merge`,
|
|
3052
|
+
"PUT",
|
|
3053
|
+
JSON.stringify({
|
|
3054
|
+
squash: input.mergeMethod === "squash",
|
|
3055
|
+
merge_when_pipeline_succeeds: input.action === "enable-auto-merge"
|
|
3056
|
+
})
|
|
3057
|
+
);
|
|
3058
|
+
return null;
|
|
3059
|
+
case "disable-auto-merge":
|
|
3060
|
+
await write(`${mergeRequestPath(input)}/cancel_merge_when_pipeline_succeeds`, "POST");
|
|
3061
|
+
return null;
|
|
3062
|
+
// rebase,因为 GitLab 没有别的办法把分支挪到目标上:它没有 GitHub 那个把目标合
|
|
3063
|
+
// 进来的更新按钮,这也是这家只声明 `rebase` 的原因。
|
|
3064
|
+
case "update-branch":
|
|
3065
|
+
await write(`${mergeRequestPath(input)}/rebase`, "PUT");
|
|
3066
|
+
return null;
|
|
3067
|
+
case "close":
|
|
3068
|
+
case "reopen":
|
|
3069
|
+
await write(mergeRequestPath(input), "PUT", JSON.stringify({ state_event: input.action }));
|
|
3070
|
+
return null;
|
|
3071
|
+
// 进出草稿要先读标题:改的是前缀,而 GitLab 只接受写回一个完整的新标题。
|
|
3072
|
+
case "ready":
|
|
3073
|
+
case "draft": {
|
|
3074
|
+
const mergeRequest = await mergeRequestDetail(ctx, input, "runAction");
|
|
3075
|
+
const title = mergeRequest.title.replace(DRAFT_PREFIX, "").trim();
|
|
3076
|
+
await write(
|
|
3077
|
+
mergeRequestPath(input),
|
|
3078
|
+
"PUT",
|
|
3079
|
+
JSON.stringify({ title: input.action === "draft" ? `Draft: ${title}` : title })
|
|
3080
|
+
);
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
var updateChangeRequest2 = async (ctx, input) => {
|
|
3086
|
+
await callGitLabApi(ctx, {
|
|
3087
|
+
operation: "updateChangeRequest",
|
|
3088
|
+
host: input.host,
|
|
3089
|
+
path: mergeRequestPath(input),
|
|
3090
|
+
method: "PUT",
|
|
3091
|
+
// 只发调用方要改的字段:GitLab 不发的就不动,发了空的就清掉——所以单独改标题时
|
|
3092
|
+
// 一个描述字段都不能带。
|
|
3093
|
+
body: JSON.stringify({
|
|
3094
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
3095
|
+
...input.body === void 0 ? {} : { description: input.body }
|
|
3096
|
+
})
|
|
3097
|
+
});
|
|
3098
|
+
return null;
|
|
3099
|
+
};
|
|
3100
|
+
var comment2 = async (ctx, input) => {
|
|
3101
|
+
await callGitLabApi(ctx, {
|
|
3102
|
+
operation: "comment",
|
|
3103
|
+
host: input.host,
|
|
3104
|
+
path: `${mergeRequestPath(input)}/notes`,
|
|
3105
|
+
method: "POST",
|
|
3106
|
+
body: JSON.stringify({ body: input.body })
|
|
3107
|
+
});
|
|
3108
|
+
return null;
|
|
3109
|
+
};
|
|
3110
|
+
var updateComment2 = async (ctx, input) => {
|
|
3111
|
+
await callGitLabApi(ctx, {
|
|
3112
|
+
operation: "updateComment",
|
|
3113
|
+
host: input.host,
|
|
3114
|
+
path: `${mergeRequestPath(input)}/notes/${encodeURIComponent(input.commentId)}`,
|
|
3115
|
+
method: "PUT",
|
|
3116
|
+
body: JSON.stringify({ body: input.body })
|
|
3117
|
+
});
|
|
3118
|
+
return null;
|
|
3119
|
+
};
|
|
3120
|
+
var getDiffRefs = async (ctx, input, operation) => {
|
|
3121
|
+
const result = await callGitLabApi(ctx, {
|
|
3122
|
+
operation,
|
|
3123
|
+
host: input.host,
|
|
3124
|
+
path: mergeRequestPath(input)
|
|
3125
|
+
});
|
|
3126
|
+
const decoded = decodeDiffRefs(result.body.trim());
|
|
3127
|
+
if (decoded === null) throw readError2(operation, "getDiffRefs");
|
|
3128
|
+
if (decoded.refs === null) {
|
|
3129
|
+
throw new GitLabError({
|
|
3130
|
+
code: "failed",
|
|
3131
|
+
operation,
|
|
3132
|
+
detail: "The merge request reported no diff revisions."
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
return decoded.refs;
|
|
3136
|
+
};
|
|
3137
|
+
var submitReview2 = async (ctx, input) => {
|
|
3138
|
+
const mergeRequest = mergeRequestPath(input);
|
|
3139
|
+
if (input.comments.length > 0) {
|
|
3140
|
+
const refs = await getDiffRefs(ctx, input, "submitReview");
|
|
3141
|
+
for (const draft of input.comments) {
|
|
3142
|
+
await callGitLabApi(ctx, {
|
|
3143
|
+
operation: "submitReview",
|
|
3144
|
+
host: input.host,
|
|
3145
|
+
path: `${mergeRequest}/discussions`,
|
|
3146
|
+
method: "POST",
|
|
3147
|
+
body: JSON.stringify({
|
|
3148
|
+
body: draft.body,
|
|
3149
|
+
position: {
|
|
3150
|
+
base_sha: refs.baseSha,
|
|
3151
|
+
head_sha: refs.headSha,
|
|
3152
|
+
start_sha: refs.startSha,
|
|
3153
|
+
position_type: "text",
|
|
3154
|
+
// 两个路径都发,因为 GitLab 对着 diff 的两侧解析位置。它们只在改名的文件
|
|
3155
|
+
// 上不同,这也是草稿带着改名前那个名字的原因。
|
|
3156
|
+
old_path: draft.oldPath ?? draft.path,
|
|
3157
|
+
new_path: draft.path,
|
|
3158
|
+
...reviewPositionLines(draft.position)
|
|
3159
|
+
}
|
|
3160
|
+
})
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (input.body.trim().length > 0) {
|
|
3165
|
+
await callGitLabApi(ctx, {
|
|
3166
|
+
operation: "submitReview",
|
|
3167
|
+
host: input.host,
|
|
3168
|
+
path: `${mergeRequest}/notes`,
|
|
3169
|
+
method: "POST",
|
|
3170
|
+
body: JSON.stringify({ body: input.body })
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
if (input.verdict === "approve") {
|
|
3174
|
+
await callGitLabApi(ctx, {
|
|
3175
|
+
operation: "submitReview",
|
|
3176
|
+
host: input.host,
|
|
3177
|
+
path: `${mergeRequest}/approve`,
|
|
3178
|
+
method: "POST"
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
return null;
|
|
3182
|
+
};
|
|
3183
|
+
var replyToThread2 = async (ctx, input) => {
|
|
3184
|
+
await callGitLabApi(ctx, {
|
|
3185
|
+
operation: "replyToThread",
|
|
3186
|
+
host: input.host,
|
|
3187
|
+
path: `${mergeRequestPath(input)}/discussions/${encodeURIComponent(input.threadId)}/notes`,
|
|
3188
|
+
method: "POST",
|
|
3189
|
+
body: JSON.stringify({ body: input.body })
|
|
3190
|
+
});
|
|
3191
|
+
return null;
|
|
3192
|
+
};
|
|
3193
|
+
var setThreadResolution2 = async (ctx, input) => {
|
|
3194
|
+
await callGitLabApi(ctx, {
|
|
3195
|
+
operation: "setThreadResolution",
|
|
3196
|
+
host: input.host,
|
|
3197
|
+
path: `${mergeRequestPath(input)}/discussions/${encodeURIComponent(input.threadId)}`,
|
|
3198
|
+
method: "PUT",
|
|
3199
|
+
body: JSON.stringify({ resolved: input.resolved })
|
|
3200
|
+
});
|
|
3201
|
+
return null;
|
|
3202
|
+
};
|
|
3203
|
+
var awardSubjectPath = (input) => input.subjectId === void 0 ? `${mergeRequestPath(input)}/award_emoji` : `${mergeRequestPath(input)}/notes/${encodeURIComponent(input.subjectId)}/award_emoji`;
|
|
3204
|
+
var setReaction2 = async (ctx, input) => {
|
|
3205
|
+
const subject = awardSubjectPath(input);
|
|
3206
|
+
if (input.reacted) {
|
|
3207
|
+
await callGitLabApi(ctx, {
|
|
3208
|
+
operation: "setReaction",
|
|
3209
|
+
host: input.host,
|
|
3210
|
+
path: `${subject}?${query([["name", gitLabAwardName(input.content)]])}`,
|
|
3211
|
+
method: "POST"
|
|
3212
|
+
});
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
3215
|
+
const viewer = await getViewer2(ctx);
|
|
3216
|
+
const listed = await callGitLabApi(ctx, {
|
|
3217
|
+
operation: "setReaction",
|
|
3218
|
+
host: input.host,
|
|
3219
|
+
path: subject
|
|
3220
|
+
});
|
|
3221
|
+
const own = decodeOwnAwardId(listed.body.trim(), { content: input.content, viewer });
|
|
3222
|
+
if (own === null) throw readError2("setReaction", "setReaction");
|
|
3223
|
+
if (own.awardId === null) return null;
|
|
3224
|
+
await callGitLabApi(ctx, {
|
|
3225
|
+
operation: "setReaction",
|
|
3226
|
+
host: input.host,
|
|
3227
|
+
path: `${subject}/${own.awardId}`,
|
|
3228
|
+
method: "DELETE"
|
|
3229
|
+
});
|
|
3230
|
+
return null;
|
|
3231
|
+
};
|
|
3232
|
+
|
|
3233
|
+
// src/service/providerApi.ts
|
|
3234
|
+
var CODES = [
|
|
3235
|
+
"missing-tool",
|
|
3236
|
+
"unauthenticated",
|
|
3237
|
+
"rate-limited",
|
|
3238
|
+
"failed"
|
|
3239
|
+
];
|
|
3240
|
+
var providerFailureOf = (error) => {
|
|
3241
|
+
const held = error;
|
|
3242
|
+
const code = CODES.find((candidate) => candidate === held?.code) ?? "failed";
|
|
3243
|
+
const retryAt = held?.data?.retryAt;
|
|
3244
|
+
return {
|
|
3245
|
+
code,
|
|
3246
|
+
detail: typeof held?.message === "string" ? held.message : String(error),
|
|
3247
|
+
...typeof retryAt === "number" && Number.isFinite(retryAt) ? { retryAt } : {}
|
|
3248
|
+
};
|
|
3249
|
+
};
|
|
3250
|
+
var gitlabApi = (ctx) => ({
|
|
3251
|
+
kind: "gitlab",
|
|
3252
|
+
capabilities: CAPABILITIES2,
|
|
3253
|
+
hosts: [configuredHost2(ctx)],
|
|
3254
|
+
getViewer: () => getViewer2(ctx),
|
|
3255
|
+
listChangeRequests: (input) => listChangeRequests2(ctx, input),
|
|
3256
|
+
getChangeRequest: (input) => getChangeRequest2(ctx, input),
|
|
3257
|
+
getChangeRequestActivity: (input) => getChangeRequestActivity2(ctx, input),
|
|
3258
|
+
getViewerPermissions: (input) => getViewerPermissions2(ctx, input),
|
|
3259
|
+
getDiff: (input) => getDiff2(ctx, input),
|
|
3260
|
+
listReviewerCandidates: (input) => listReviewerCandidates2(ctx, input),
|
|
3261
|
+
setReviewerRequest: (input) => setReviewerRequest2(ctx, input),
|
|
3262
|
+
runAction: (input) => runAction2(ctx, input),
|
|
3263
|
+
updateChangeRequest: (input) => updateChangeRequest2(ctx, input),
|
|
3264
|
+
comment: (input) => comment2(ctx, input),
|
|
3265
|
+
updateComment: (input) => updateComment2(ctx, input),
|
|
3266
|
+
submitReview: (input) => submitReview2(ctx, input),
|
|
3267
|
+
replyToThread: (input) => replyToThread2(ctx, input),
|
|
3268
|
+
setReaction: (input) => setReaction2(ctx, input),
|
|
3269
|
+
setThreadResolution: (input) => setThreadResolution2(ctx, input)
|
|
3270
|
+
});
|
|
3271
|
+
var githubApi = (ctx) => ({
|
|
3272
|
+
kind: "github",
|
|
3273
|
+
capabilities: CAPABILITIES,
|
|
3274
|
+
hosts: [configuredHost(ctx)],
|
|
3275
|
+
getViewer: () => getViewer(ctx),
|
|
3276
|
+
listChangeRequests: (input) => listChangeRequests(ctx, input),
|
|
3277
|
+
listChangeRequestsAcross: (input) => listChangeRequestsAcross(ctx, input),
|
|
3278
|
+
listChangeRequestStats: (input) => listChangeRequestStats(ctx, input),
|
|
3279
|
+
getChangeRequest: (input) => getChangeRequest(ctx, input),
|
|
3280
|
+
getChangeRequestActivity: (input) => getChangeRequestActivity(ctx, input),
|
|
3281
|
+
getReviewThreadComments: (input) => getReviewThreadComments(ctx, input),
|
|
3282
|
+
getViewerPermissions: (input) => getViewerPermissions(ctx, input),
|
|
3283
|
+
getDiff: (input) => getDiff(ctx, input),
|
|
3284
|
+
getDiffFileContents: (input) => getDiffFileContents(ctx, input),
|
|
3285
|
+
listReviewerCandidates: (input) => listReviewerCandidates(ctx, input),
|
|
3286
|
+
setReviewerRequest: (input) => setReviewerRequest(ctx, input),
|
|
3287
|
+
runAction: (input) => runAction(ctx, input),
|
|
3288
|
+
updateChangeRequest: (input) => updateChangeRequest(ctx, input),
|
|
3289
|
+
comment: (input) => comment(ctx, input),
|
|
3290
|
+
updateComment: (input) => updateComment(ctx, input),
|
|
3291
|
+
submitReview: (input) => submitReview(ctx, input),
|
|
3292
|
+
replyToThread: (input) => replyToThread(ctx, input),
|
|
3293
|
+
setReaction: (input) => setReaction(ctx, input),
|
|
3294
|
+
setThreadResolution: (input) => setThreadResolution(ctx, input)
|
|
3295
|
+
});
|
|
3296
|
+
var ProviderRateLimited = class extends Error {
|
|
3297
|
+
code = "rate-limited";
|
|
3298
|
+
data;
|
|
3299
|
+
constructor(input) {
|
|
3300
|
+
super(`Rate limited in ${input.operation}: ${input.detail}`);
|
|
3301
|
+
this.name = "ProviderRateLimited";
|
|
3302
|
+
this.data = { retryAt: input.retryAt };
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
var withRateLimitBackoff = (api, host, backoff) => {
|
|
3306
|
+
const key = { provider: api.kind, host };
|
|
3307
|
+
const wrap = (operation, call, allowPaused = false) => async (input) => {
|
|
3308
|
+
const gate = backoff.check(key, { allowPaused });
|
|
3309
|
+
if (isPaused(gate)) {
|
|
3310
|
+
throw new ProviderRateLimited({
|
|
3311
|
+
operation,
|
|
3312
|
+
detail: `${api.kind} requests to ${gate.host} are paused until the rate limit resets.`,
|
|
3313
|
+
retryAt: gate.retryAt
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
try {
|
|
3317
|
+
const value = await call(input);
|
|
3318
|
+
backoff.recordSuccess({ ...key, lease: gate.lease });
|
|
3319
|
+
return value;
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
const failure = providerFailureOf(error);
|
|
3322
|
+
if (failure.code === "rate-limited") {
|
|
3323
|
+
backoff.recordRateLimit({ ...key, lease: gate.lease, retryAt: failure.retryAt });
|
|
3324
|
+
}
|
|
3325
|
+
throw error;
|
|
3326
|
+
}
|
|
3327
|
+
};
|
|
3328
|
+
const across = api.listChangeRequestsAcross;
|
|
3329
|
+
const stats = api.listChangeRequestStats;
|
|
3330
|
+
const threadComments = api.getReviewThreadComments;
|
|
3331
|
+
const fileContents = api.getDiffFileContents;
|
|
3332
|
+
const rewrite = api.updateChangeRequest;
|
|
3333
|
+
const rewriteComment = api.updateComment;
|
|
3334
|
+
return {
|
|
3335
|
+
kind: api.kind,
|
|
3336
|
+
capabilities: api.capabilities,
|
|
3337
|
+
hosts: api.hosts,
|
|
3338
|
+
getViewer: wrap("getViewer", api.getViewer),
|
|
3339
|
+
listChangeRequests: wrap("listChangeRequests", api.listChangeRequests),
|
|
3340
|
+
...across === void 0 ? {} : { listChangeRequestsAcross: wrap("listChangeRequestsAcross", across) },
|
|
3341
|
+
...stats === void 0 ? {} : { listChangeRequestStats: wrap("listChangeRequestStats", stats) },
|
|
3342
|
+
getChangeRequest: wrap("getChangeRequest", api.getChangeRequest),
|
|
3343
|
+
getChangeRequestActivity: wrap("getChangeRequestActivity", api.getChangeRequestActivity),
|
|
3344
|
+
...threadComments === void 0 ? {} : { getReviewThreadComments: wrap("getReviewThreadComments", threadComments) },
|
|
3345
|
+
getViewerPermissions: wrap("getViewerPermissions", api.getViewerPermissions, true),
|
|
3346
|
+
getDiff: wrap("getDiff", api.getDiff),
|
|
3347
|
+
...fileContents === void 0 ? {} : { getDiffFileContents: wrap("getDiffFileContents", fileContents) },
|
|
3348
|
+
listReviewerCandidates: wrap("listReviewerCandidates", api.listReviewerCandidates, true),
|
|
3349
|
+
setReviewerRequest: wrap("setReviewerRequest", api.setReviewerRequest, true),
|
|
3350
|
+
runAction: wrap("runAction", api.runAction, true),
|
|
3351
|
+
...rewrite === void 0 ? {} : { updateChangeRequest: wrap("updateChangeRequest", rewrite, true) },
|
|
3352
|
+
comment: wrap("comment", api.comment, true),
|
|
3353
|
+
...rewriteComment === void 0 ? {} : { updateComment: wrap("updateComment", rewriteComment, true) },
|
|
3354
|
+
submitReview: wrap("submitReview", api.submitReview, true),
|
|
3355
|
+
replyToThread: wrap("replyToThread", api.replyToThread, true),
|
|
3356
|
+
setReaction: wrap("setReaction", api.setReaction, true),
|
|
3357
|
+
setThreadResolution: wrap("setThreadResolution", api.setThreadResolution, true)
|
|
3358
|
+
};
|
|
3359
|
+
};
|
|
3360
|
+
|
|
3361
|
+
// src/service/workspace.ts
|
|
3362
|
+
var repositoryIdentityOf = (identity) => {
|
|
3363
|
+
if (identity.displayName) return identity.displayName;
|
|
3364
|
+
return identity.owner && identity.name ? `${identity.owner}/${identity.name}` : null;
|
|
3365
|
+
};
|
|
3366
|
+
var hostOf = (identity, kind) => {
|
|
3367
|
+
const host = identity.canonicalKey.split("/")[0]?.trim();
|
|
3368
|
+
return host === void 0 || host.length === 0 ? kind : host.toLowerCase();
|
|
3369
|
+
};
|
|
3370
|
+
var remoteHostOf = (remoteUrl) => {
|
|
3371
|
+
const normalized = remoteUrl.trim().replace(/\/+$/g, "").toLowerCase();
|
|
3372
|
+
if (/^(?:ssh|https?|git):\/\//i.test(normalized)) {
|
|
3373
|
+
try {
|
|
3374
|
+
return new URL(normalized).hostname;
|
|
3375
|
+
} catch {
|
|
3376
|
+
return "";
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return /^[a-z0-9._-]+@([^:/\s]+):/i.exec(normalized)?.[1] ?? "";
|
|
3380
|
+
};
|
|
3381
|
+
var claimTable = (apis) => {
|
|
3382
|
+
const claims = /* @__PURE__ */ new Map();
|
|
3383
|
+
for (const api of apis) {
|
|
3384
|
+
for (const host of api.hosts) {
|
|
3385
|
+
if (host.length > 0 && !claims.has(host)) claims.set(host, api.kind);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
return claims;
|
|
3389
|
+
};
|
|
3390
|
+
var listWorkspaceProjects = async (input) => {
|
|
3391
|
+
const projects = await input.listProjects();
|
|
3392
|
+
const claims = claimTable(input.apis);
|
|
3393
|
+
const byKind = new Map(input.apis.map((api) => [api.kind, api]));
|
|
3394
|
+
const supported = [];
|
|
3395
|
+
const unimplemented = /* @__PURE__ */ new Map();
|
|
3396
|
+
const viewerRoots = /* @__PURE__ */ new Map();
|
|
3397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3398
|
+
for (const project of projects) {
|
|
3399
|
+
if (input.filter.projectId !== void 0 && project.projectId !== input.filter.projectId) {
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
if (input.filter.projectIds !== void 0 && !input.filter.projectIds.includes(project.projectId)) {
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
const identity = project.repositoryIdentity;
|
|
3406
|
+
if (identity === null) continue;
|
|
3407
|
+
const repository = repositoryIdentityOf(identity);
|
|
3408
|
+
if (repository === null) continue;
|
|
3409
|
+
let kind = identity.provider;
|
|
3410
|
+
if (kind === "unknown") {
|
|
3411
|
+
const claimed = claims.get(remoteHostOf(identity.remoteUrl));
|
|
3412
|
+
if (claimed !== void 0) kind = claimed;
|
|
3413
|
+
}
|
|
3414
|
+
const host = hostOf(identity, kind);
|
|
3415
|
+
if (input.filter.host !== void 0 && host !== input.filter.host.trim().toLowerCase()) {
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
const api = byKind.get(kind) ?? null;
|
|
3419
|
+
if (api !== null) {
|
|
3420
|
+
const roots = viewerRoots.get(host);
|
|
3421
|
+
if (roots === void 0) viewerRoots.set(host, [project.workspaceRoot]);
|
|
3422
|
+
else if (!roots.includes(project.workspaceRoot)) roots.push(project.workspaceRoot);
|
|
3423
|
+
}
|
|
3424
|
+
const key = listCursorKey(host, repository);
|
|
3425
|
+
if (seen.has(key)) continue;
|
|
3426
|
+
seen.add(key);
|
|
3427
|
+
if (api === null) {
|
|
3428
|
+
const counted = unimplemented.get(host);
|
|
3429
|
+
if (counted === void 0) unimplemented.set(host, { kind, projectCount: 1 });
|
|
3430
|
+
else counted.projectCount += 1;
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
supported.push({
|
|
3434
|
+
projectId: project.projectId,
|
|
3435
|
+
projectTitle: project.title,
|
|
3436
|
+
workspaceRoot: project.workspaceRoot,
|
|
3437
|
+
api: withRateLimitBackoff(api, host, input.backoff),
|
|
3438
|
+
repository,
|
|
3439
|
+
host
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
return { supported, unimplemented, viewerRoots };
|
|
3443
|
+
};
|
|
3444
|
+
|
|
3445
|
+
// src/service/pullRequestService.ts
|
|
3446
|
+
var DEFAULT_REPOSITORY_LIST_LIMIT = 99;
|
|
3447
|
+
var REPOSITORY_CONCURRENCY = 12;
|
|
3448
|
+
var REPOSITORY_SEARCH_CHUNK = 100;
|
|
3449
|
+
var LIST_CACHE_TTL_MS = 3e4;
|
|
3450
|
+
var DETAIL_CACHE_TTL_MS = 15e3;
|
|
3451
|
+
var DIFF_CACHE_TTL_MS = 6e4;
|
|
3452
|
+
var COMMIT_DIFF_CACHE_TTL_MS = 10 * 6e4;
|
|
3453
|
+
var LIST_STATS_CACHE_TTL_MS = 6e4;
|
|
3454
|
+
var DIFF_STALE_WINDOW_MS = 10 * 6e4;
|
|
3455
|
+
var VIEWER_CACHE_TTL_MS = 10 * 6e4;
|
|
3456
|
+
var LIST_CACHE_CAPACITY = 64;
|
|
3457
|
+
var LIST_STATS_CACHE_CAPACITY = 32;
|
|
3458
|
+
var DETAIL_CACHE_CAPACITY = 128;
|
|
3459
|
+
var DIFF_CACHE_CAPACITY = 128;
|
|
3460
|
+
var REF_EPOCH_CAPACITY = 2048;
|
|
3461
|
+
var VERDICT_LABELS = {
|
|
3462
|
+
comment: "review",
|
|
3463
|
+
approve: "approve",
|
|
3464
|
+
"request-changes": "request changes on"
|
|
3465
|
+
};
|
|
3466
|
+
var ACTION_ACCESS_REFUSALS = {
|
|
3467
|
+
merge: "You need write access on this repository to merge.",
|
|
3468
|
+
ready: "You need write access on this repository, or to have opened this change request, to mark it ready for review.",
|
|
3469
|
+
draft: "You need write access on this repository, or to have opened this change request, to return it to a draft.",
|
|
3470
|
+
close: "You need write access on this repository, or to have opened this change request, to close it.",
|
|
3471
|
+
"update-branch": "You need write access on this repository, or to have opened this change request, to update its branch.",
|
|
3472
|
+
reopen: "You need write access on this repository, or to have opened this change request, to reopen it.",
|
|
3473
|
+
"enable-auto-merge": "You need write access on this repository to have it merged for you once it is ready.",
|
|
3474
|
+
"disable-auto-merge": "You need write access on this repository to stop it being merged for you once it is ready."
|
|
3475
|
+
};
|
|
3476
|
+
var REVIEWER_REQUEST_REFUSAL = "You need write access on this repository to ask for a review.";
|
|
3477
|
+
var mapConcurrent = async (items, limit, run) => {
|
|
3478
|
+
const results = Array.from({ length: items.length });
|
|
3479
|
+
let next = 0;
|
|
3480
|
+
await Promise.all(
|
|
3481
|
+
Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
3482
|
+
for (; ; ) {
|
|
3483
|
+
const index = next++;
|
|
3484
|
+
if (index >= items.length) return;
|
|
3485
|
+
results[index] = await run(items[index]);
|
|
3486
|
+
}
|
|
3487
|
+
})
|
|
3488
|
+
);
|
|
3489
|
+
return results;
|
|
3490
|
+
};
|
|
3491
|
+
var firstSuccess = async (runs) => {
|
|
3492
|
+
let last = new Error("nothing to try");
|
|
3493
|
+
for (const run of runs) {
|
|
3494
|
+
try {
|
|
3495
|
+
return await run();
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
last = error;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
throw last;
|
|
3501
|
+
};
|
|
3502
|
+
var isProviderUnusable = (failure) => failure.code === "missing-tool" || failure.code === "unauthenticated";
|
|
3503
|
+
var unavailableReasonOf = (failure) => failure.code === "missing-tool" ? "token-missing" : "token-rejected";
|
|
3504
|
+
var providerDetail = (failure, kind) => isProviderUnusable(failure) ? providerRequirement(kind, unavailableReasonOf(failure)) ?? failure.detail : failure.detail;
|
|
3505
|
+
var toPullRequestError = (operation, error, kind, host) => {
|
|
3506
|
+
const failure = providerFailureOf(error);
|
|
3507
|
+
if (isProviderUnusable(failure)) {
|
|
3508
|
+
return new PullRequestUnavailable({
|
|
3509
|
+
reason: unavailableReasonOf(failure),
|
|
3510
|
+
provider: kind,
|
|
3511
|
+
...host === void 0 ? {} : { host }
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
return new PullRequestOperationFailed({
|
|
3515
|
+
operation,
|
|
3516
|
+
detail: failure.detail,
|
|
3517
|
+
...failure.retryAt === void 0 ? {} : { retryAt: failure.retryAt }
|
|
3518
|
+
});
|
|
3519
|
+
};
|
|
3520
|
+
var resolveAuthorFilter = (author, viewer) => {
|
|
3521
|
+
const trimmed = author.trim();
|
|
3522
|
+
if (!/^@?me$/iu.test(trimmed)) return trimmed;
|
|
3523
|
+
return viewer === null || viewer.trim().length === 0 ? trimmed : viewer;
|
|
3524
|
+
};
|
|
3525
|
+
var matchesRowFilters = (item, filters, viewer) => {
|
|
3526
|
+
if (filters === void 0) return true;
|
|
3527
|
+
const labels = new Set(item.labels.map((label) => label.name.trim().toLowerCase()));
|
|
3528
|
+
const holds = (label) => labels.has(label.trim().toLowerCase());
|
|
3529
|
+
return (filters.draft === void 0 || item.isDraft === (filters.draft === "only")) && // 判在 provider 行上而不是 entry 上:`null` 是「会汇总评审的主机说还没有结论」,
|
|
3530
|
+
// 也就是 "none" 问的那个;`undefined` 是「这台主机根本不汇总」,那种行不判。
|
|
3531
|
+
(filters.review === void 0 || item.reviewDecision === void 0 || (filters.review === "none" ? item.reviewDecision === null : item.reviewDecision === filters.review)) && (filters.labels === void 0 || filters.labels.every((group) => group.some(holds))) && (filters.excludedLabels === void 0 || !filters.excludedLabels.some(holds)) && (filters.author === void 0 || item.author?.login.toLowerCase() === resolveAuthorFilter(filters.author, viewer).toLowerCase());
|
|
3532
|
+
};
|
|
3533
|
+
var createPullRequestService = (options) => {
|
|
3534
|
+
const now = options.now ?? Date.now;
|
|
3535
|
+
const backoff = createBackoff({ now });
|
|
3536
|
+
const workspace = (filter) => listWorkspaceProjects({
|
|
3537
|
+
listProjects: options.listProjects,
|
|
3538
|
+
apis: options.apis,
|
|
3539
|
+
backoff,
|
|
3540
|
+
filter
|
|
3541
|
+
}).catch(() => {
|
|
3542
|
+
throw new PullRequestOperationFailed({
|
|
3543
|
+
operation: "listProjects",
|
|
3544
|
+
detail: "The project list could not be read."
|
|
3545
|
+
});
|
|
3546
|
+
});
|
|
3547
|
+
const requireProject = async (ref) => {
|
|
3548
|
+
const { supported } = await workspace({ projectId: ref.projectId });
|
|
3549
|
+
const match = supported[0];
|
|
3550
|
+
if (!match) throw new PullRequestUnavailable({ reason: "provider-unsupported" });
|
|
3551
|
+
if (match.repository.toLowerCase() !== ref.repository.trim().toLowerCase()) {
|
|
3552
|
+
throw new PullRequestOperationFailed({
|
|
3553
|
+
operation: "resolveRepository",
|
|
3554
|
+
detail: "The change request does not belong to the selected project."
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
return match;
|
|
3558
|
+
};
|
|
3559
|
+
const changeRequestRef = (project, ref) => ({
|
|
3560
|
+
cwd: project.workspaceRoot,
|
|
3561
|
+
repository: project.repository,
|
|
3562
|
+
host: project.host,
|
|
3563
|
+
number: ref.number
|
|
3564
|
+
});
|
|
3565
|
+
const viewerPermissionsOf = async (project, ref, operation) => {
|
|
3566
|
+
try {
|
|
3567
|
+
return await project.api.getViewerPermissions(changeRequestRef(project, ref));
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
throw toPullRequestError(operation, error, project.api.kind, project.host);
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
const decodeCursors = (cursors) => {
|
|
3573
|
+
if (cursors === void 0) return null;
|
|
3574
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
3575
|
+
for (const [key, raw] of Object.entries(cursors)) {
|
|
3576
|
+
const cursor = parseListCursor(raw);
|
|
3577
|
+
if (cursor === null) {
|
|
3578
|
+
throw new PullRequestOperationFailed({
|
|
3579
|
+
operation: "list",
|
|
3580
|
+
detail: "The list could not be carried on from where it left off."
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
decoded.set(key, cursor);
|
|
3584
|
+
}
|
|
3585
|
+
return decoded;
|
|
3586
|
+
};
|
|
3587
|
+
const viewersByHost = /* @__PURE__ */ new Map();
|
|
3588
|
+
const resolveViewers = (projects, viewerRoots) => mapConcurrent(
|
|
3589
|
+
[...new Set(projects.map(({ host }) => host))],
|
|
3590
|
+
REPOSITORY_CONCURRENCY,
|
|
3591
|
+
async (host) => {
|
|
3592
|
+
const held = viewersByHost.get(host);
|
|
3593
|
+
if (held !== void 0 && now() - held.at <= VIEWER_CACHE_TTL_MS) return held.result;
|
|
3594
|
+
const forHost = projects.filter((project) => project.host === host);
|
|
3595
|
+
const api = forHost[0].api;
|
|
3596
|
+
const roots = viewerRoots.get(host) ?? forHost.map(({ workspaceRoot }) => workspaceRoot);
|
|
3597
|
+
try {
|
|
3598
|
+
const viewer = await firstSuccess(roots.map((cwd) => () => api.getViewer({ cwd })));
|
|
3599
|
+
const result = { host, kind: api.kind, viewer, error: null };
|
|
3600
|
+
viewersByHost.set(host, { at: now(), result });
|
|
3601
|
+
return result;
|
|
3602
|
+
} catch (error) {
|
|
3603
|
+
return { host, kind: api.kind, viewer: null, error: providerFailureOf(error) };
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
);
|
|
3607
|
+
const viewerOf = async (project) => {
|
|
3608
|
+
const [resolved] = await resolveViewers([project], /* @__PURE__ */ new Map());
|
|
3609
|
+
return resolved?.viewer ?? null;
|
|
3610
|
+
};
|
|
3611
|
+
const toEntry = (input) => {
|
|
3612
|
+
const viewer = input.viewer.toLowerCase();
|
|
3613
|
+
const { item, project } = input;
|
|
3614
|
+
return {
|
|
3615
|
+
provider: project.api.kind,
|
|
3616
|
+
host: project.host,
|
|
3617
|
+
projectId: project.projectId,
|
|
3618
|
+
projectTitle: project.projectTitle,
|
|
3619
|
+
repository: project.repository,
|
|
3620
|
+
number: item.number,
|
|
3621
|
+
title: item.title,
|
|
3622
|
+
url: item.url,
|
|
3623
|
+
author: item.author,
|
|
3624
|
+
headBranch: item.headBranch,
|
|
3625
|
+
baseBranch: item.baseBranch,
|
|
3626
|
+
state: item.state,
|
|
3627
|
+
isDraft: item.isDraft,
|
|
3628
|
+
mergeability: item.mergeability,
|
|
3629
|
+
additions: item.additions,
|
|
3630
|
+
deletions: item.deletions,
|
|
3631
|
+
createdAt: item.createdAt,
|
|
3632
|
+
updatedAt: item.updatedAt,
|
|
3633
|
+
...item.checksState === void 0 || item.checksState === null ? {} : { checksState: item.checksState },
|
|
3634
|
+
viewerReviewRequested: item.author?.login.toLowerCase() !== viewer && item.reviewRequestLogins.some((login) => login.toLowerCase() === viewer),
|
|
3635
|
+
labels: item.labels,
|
|
3636
|
+
...item.reviewDecision === void 0 || item.reviewDecision === null ? {} : { reviewDecision: item.reviewDecision }
|
|
3637
|
+
};
|
|
3638
|
+
};
|
|
3639
|
+
const listUncached = async (input) => {
|
|
3640
|
+
const involvement = input.involvement ?? "all";
|
|
3641
|
+
const continuation = decodeCursors(input.cursors);
|
|
3642
|
+
const { supported: projects, unimplemented, viewerRoots } = await workspace(input);
|
|
3643
|
+
const projectCounts = /* @__PURE__ */ new Map();
|
|
3644
|
+
for (const { host } of projects) projectCounts.set(host, (projectCounts.get(host) ?? 0) + 1);
|
|
3645
|
+
const viewerResults = await resolveViewers(projects, viewerRoots);
|
|
3646
|
+
const viewers = {};
|
|
3647
|
+
for (const result of viewerResults) {
|
|
3648
|
+
if (result.viewer !== null) viewers[result.host] = result.viewer;
|
|
3649
|
+
}
|
|
3650
|
+
const providers = [
|
|
3651
|
+
...viewerResults.map((result) => ({
|
|
3652
|
+
host: result.host,
|
|
3653
|
+
kind: result.kind,
|
|
3654
|
+
searchesOnHost: projects.find((project) => project.host === result.host)?.api.capabilities.search ?? false,
|
|
3655
|
+
projectCount: projectCounts.get(result.host) ?? 1,
|
|
3656
|
+
configured: result.viewer !== null,
|
|
3657
|
+
detail: result.error === null ? null : providerDetail(result.error, result.kind)
|
|
3658
|
+
})),
|
|
3659
|
+
...[...unimplemented].map(([host, { kind, projectCount }]) => ({
|
|
3660
|
+
host,
|
|
3661
|
+
kind,
|
|
3662
|
+
searchesOnHost: false,
|
|
3663
|
+
projectCount,
|
|
3664
|
+
configured: false,
|
|
3665
|
+
detail: "This host cannot be browsed here yet."
|
|
3666
|
+
}))
|
|
3667
|
+
];
|
|
3668
|
+
const selected = continuation === null ? projects : projects.filter(
|
|
3669
|
+
({ host, repository }) => continuation.has(listCursorKey(host, repository))
|
|
3670
|
+
);
|
|
3671
|
+
const readable = selected.filter(({ host }) => viewers[host] !== void 0);
|
|
3672
|
+
const unreadable = selected.filter(({ host }) => viewers[host] === void 0).map(({ projectId, projectTitle, repository }) => ({
|
|
3673
|
+
projectId,
|
|
3674
|
+
projectTitle,
|
|
3675
|
+
message: `${repository} could not be read.`
|
|
3676
|
+
}));
|
|
3677
|
+
if (readable.length === 0) {
|
|
3678
|
+
const errors = viewerResults.flatMap(
|
|
3679
|
+
(result) => result.error === null || !selected.some(({ host }) => host === result.host) ? [] : [{ failure: result.error, kind: result.kind, host: result.host }]
|
|
3680
|
+
);
|
|
3681
|
+
const blocking = errors.find((entry) => isProviderUnusable(entry.failure)) ?? errors[0];
|
|
3682
|
+
if (blocking) {
|
|
3683
|
+
const failure = blocking.failure;
|
|
3684
|
+
if (isProviderUnusable(failure)) {
|
|
3685
|
+
throw new PullRequestUnavailable({
|
|
3686
|
+
reason: unavailableReasonOf(failure),
|
|
3687
|
+
provider: blocking.kind,
|
|
3688
|
+
host: blocking.host
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
throw new PullRequestOperationFailed({
|
|
3692
|
+
operation: "list",
|
|
3693
|
+
detail: failure.detail,
|
|
3694
|
+
...failure.retryAt === void 0 ? {} : { retryAt: failure.retryAt }
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
return { viewers, providers, entries: [], errors: [], truncated: false, nextCursors: {} };
|
|
3698
|
+
}
|
|
3699
|
+
const limit = input.limit ?? DEFAULT_REPOSITORY_LIST_LIMIT;
|
|
3700
|
+
const cursorOf = (project) => continuation?.get(listCursorKey(project.host, project.repository));
|
|
3701
|
+
const readRepository = async (project) => {
|
|
3702
|
+
const viewer = viewers[project.host];
|
|
3703
|
+
const key = listCursorKey(project.host, project.repository);
|
|
3704
|
+
const cursor = cursorOf(project);
|
|
3705
|
+
try {
|
|
3706
|
+
const page = await project.api.listChangeRequests({
|
|
3707
|
+
cwd: project.workspaceRoot,
|
|
3708
|
+
repository: project.repository,
|
|
3709
|
+
host: project.host,
|
|
3710
|
+
state: input.state,
|
|
3711
|
+
involvement,
|
|
3712
|
+
viewer,
|
|
3713
|
+
limit,
|
|
3714
|
+
query: input.query,
|
|
3715
|
+
filters: input.filters,
|
|
3716
|
+
// 只递主机能用上的那两个字段:边界那一刻已经发过哪几行是服务自己的账。
|
|
3717
|
+
...cursor === void 0 ? {} : { cursor: { updatedBefore: cursor.updatedBefore, delivered: cursor.delivered } }
|
|
3718
|
+
});
|
|
3719
|
+
const items = withoutAlreadySent(cursor, page.items);
|
|
3720
|
+
return {
|
|
3721
|
+
key,
|
|
3722
|
+
entries: items.filter((item) => matchesRowFilters(item, input.filters, viewer)).map((item) => toEntry({ project, item, viewer })),
|
|
3723
|
+
errors: [],
|
|
3724
|
+
truncated: page.truncated,
|
|
3725
|
+
nextCursor: page.continues && page.truncated ? nextListCursor(cursor, page.items, items, page.cursorAdvance) : null
|
|
3726
|
+
};
|
|
3727
|
+
} catch {
|
|
3728
|
+
return {
|
|
3729
|
+
key,
|
|
3730
|
+
entries: [],
|
|
3731
|
+
errors: [
|
|
3732
|
+
{
|
|
3733
|
+
projectId: project.projectId,
|
|
3734
|
+
projectTitle: project.projectTitle,
|
|
3735
|
+
message: `${project.repository} could not be read.`
|
|
3736
|
+
}
|
|
3737
|
+
],
|
|
3738
|
+
truncated: false,
|
|
3739
|
+
nextCursor: null
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3743
|
+
const readTogether = async (chunk) => {
|
|
3744
|
+
const first = chunk[0];
|
|
3745
|
+
const readAcross = first.api.listChangeRequestsAcross;
|
|
3746
|
+
const separately = () => mapConcurrent(chunk, REPOSITORY_CONCURRENCY, readRepository);
|
|
3747
|
+
if (readAcross === void 0) return separately();
|
|
3748
|
+
const viewer = viewers[first.host];
|
|
3749
|
+
const cursor = cursorOf(first);
|
|
3750
|
+
let page;
|
|
3751
|
+
try {
|
|
3752
|
+
page = await readAcross({
|
|
3753
|
+
cwd: first.workspaceRoot,
|
|
3754
|
+
host: first.host,
|
|
3755
|
+
repositories: chunk.map((project) => project.repository),
|
|
3756
|
+
state: input.state,
|
|
3757
|
+
involvement,
|
|
3758
|
+
viewer,
|
|
3759
|
+
limit,
|
|
3760
|
+
query: input.query,
|
|
3761
|
+
filters: input.filters,
|
|
3762
|
+
...cursor === void 0 ? {} : { cursor: { updatedBefore: cursor.updatedBefore, delivered: cursor.delivered } }
|
|
3763
|
+
});
|
|
3764
|
+
} catch {
|
|
3765
|
+
return separately();
|
|
3766
|
+
}
|
|
3767
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3768
|
+
for (const item of page.items) {
|
|
3769
|
+
const key = item.repository.trim().toLowerCase();
|
|
3770
|
+
const held = rows.get(key);
|
|
3771
|
+
if (held === void 0) rows.set(key, [item]);
|
|
3772
|
+
else held.push(item);
|
|
3773
|
+
}
|
|
3774
|
+
const boundary = page.items.reduce(
|
|
3775
|
+
(oldest, item) => oldest === null || item.updatedAt < oldest ? item.updatedAt : oldest,
|
|
3776
|
+
null
|
|
3777
|
+
);
|
|
3778
|
+
return mapConcurrent(chunk, REPOSITORY_CONCURRENCY, async (project) => {
|
|
3779
|
+
const fetched = rows.get(project.repository.trim().toLowerCase()) ?? [];
|
|
3780
|
+
if (fetched.length === 0 && cursorOf(project) === void 0) return readRepository(project);
|
|
3781
|
+
const cursorHere = cursorOf(project);
|
|
3782
|
+
const items = withoutAlreadySent(cursorHere, fetched);
|
|
3783
|
+
return {
|
|
3784
|
+
key: listCursorKey(project.host, project.repository),
|
|
3785
|
+
entries: items.filter((item) => matchesRowFilters(item, input.filters, viewer)).map((item) => toEntry({ project, item, viewer })),
|
|
3786
|
+
errors: [],
|
|
3787
|
+
truncated: page.truncated,
|
|
3788
|
+
nextCursor: page.truncated && boundary !== null ? listCursorAt(cursorHere, boundary, fetched, items.length) : null
|
|
3789
|
+
};
|
|
3790
|
+
});
|
|
3791
|
+
};
|
|
3792
|
+
const together = /* @__PURE__ */ new Map();
|
|
3793
|
+
const separate = [];
|
|
3794
|
+
for (const project of readable) {
|
|
3795
|
+
if (project.api.listChangeRequestsAcross === void 0) {
|
|
3796
|
+
separate.push(project);
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
const key = `${project.host}
|
|
3800
|
+
${cursorOf(project)?.updatedBefore ?? ""}`;
|
|
3801
|
+
const group = together.get(key);
|
|
3802
|
+
if (group === void 0) together.set(key, [project]);
|
|
3803
|
+
else group.push(project);
|
|
3804
|
+
}
|
|
3805
|
+
const reads = separate.map(
|
|
3806
|
+
(project) => () => readRepository(project).then((batch) => [batch])
|
|
3807
|
+
);
|
|
3808
|
+
for (const group of together.values()) {
|
|
3809
|
+
for (let start = 0; start < group.length; start += REPOSITORY_SEARCH_CHUNK) {
|
|
3810
|
+
const chunk = group.slice(start, start + REPOSITORY_SEARCH_CHUNK);
|
|
3811
|
+
reads.push(() => readTogether(chunk));
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
const batches = (await mapConcurrent(reads, REPOSITORY_CONCURRENCY, (read) => read())).flat();
|
|
3815
|
+
const nextCursors = {};
|
|
3816
|
+
for (const batch of batches) {
|
|
3817
|
+
if (batch.nextCursor !== null) nextCursors[batch.key] = batch.nextCursor;
|
|
3818
|
+
}
|
|
3819
|
+
return {
|
|
3820
|
+
viewers,
|
|
3821
|
+
providers,
|
|
3822
|
+
entries: batches.flatMap((batch) => batch.entries).toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
|
|
3823
|
+
errors: [...unreadable, ...batches.flatMap((batch) => batch.errors)],
|
|
3824
|
+
truncated: batches.some((batch) => batch.truncated),
|
|
3825
|
+
nextCursors
|
|
3826
|
+
};
|
|
3827
|
+
};
|
|
3828
|
+
const detailUncached = async (input) => {
|
|
3829
|
+
const project = await requireProject(input);
|
|
3830
|
+
const [changeRequest, viewer] = await Promise.all([
|
|
3831
|
+
project.api.getChangeRequest(changeRequestRef(project, input)).catch((error) => {
|
|
3832
|
+
throw toPullRequestError("detail", error, project.api.kind, project.host);
|
|
3833
|
+
}),
|
|
3834
|
+
viewerOf(project)
|
|
3835
|
+
]);
|
|
3836
|
+
return {
|
|
3837
|
+
provider: project.api.kind,
|
|
3838
|
+
capabilities: project.api.capabilities,
|
|
3839
|
+
projectId: project.projectId,
|
|
3840
|
+
projectTitle: project.projectTitle,
|
|
3841
|
+
workspaceRoot: project.workspaceRoot,
|
|
3842
|
+
repository: project.repository,
|
|
3843
|
+
number: changeRequest.number,
|
|
3844
|
+
title: changeRequest.title,
|
|
3845
|
+
body: changeRequest.body,
|
|
3846
|
+
url: changeRequest.url,
|
|
3847
|
+
author: changeRequest.author,
|
|
3848
|
+
state: changeRequest.state,
|
|
3849
|
+
isDraft: changeRequest.isDraft,
|
|
3850
|
+
mergeability: changeRequest.mergeability,
|
|
3851
|
+
additions: changeRequest.additions,
|
|
3852
|
+
deletions: changeRequest.deletions,
|
|
3853
|
+
changedFiles: changeRequest.changedFiles,
|
|
3854
|
+
headBranch: changeRequest.headBranch,
|
|
3855
|
+
baseBranch: changeRequest.baseBranch,
|
|
3856
|
+
createdAt: changeRequest.createdAt,
|
|
3857
|
+
updatedAt: changeRequest.updatedAt,
|
|
3858
|
+
mergedAt: changeRequest.mergedAt,
|
|
3859
|
+
closedAt: changeRequest.closedAt,
|
|
3860
|
+
reviewers: changeRequest.reviewers,
|
|
3861
|
+
labels: changeRequest.labels,
|
|
3862
|
+
checks: changeRequest.checks,
|
|
3863
|
+
mergeCapabilities: changeRequest.mergeCapabilities,
|
|
3864
|
+
viewerPermissions: changeRequest.viewerPermissions,
|
|
3865
|
+
...viewer === null || viewer.trim().length === 0 ? {} : { viewer },
|
|
3866
|
+
...changeRequest.baseComparison === void 0 ? {} : { baseComparison: changeRequest.baseComparison },
|
|
3867
|
+
...changeRequest.behindBy === void 0 ? {} : { behindBy: changeRequest.behindBy },
|
|
3868
|
+
...changeRequest.autoMergeEnabled === void 0 ? {} : { autoMergeEnabled: changeRequest.autoMergeEnabled }
|
|
3869
|
+
};
|
|
3870
|
+
};
|
|
3871
|
+
const activityUncached = async (input) => {
|
|
3872
|
+
const project = await requireProject(input);
|
|
3873
|
+
const activity2 = await project.api.getChangeRequestActivity(changeRequestRef(project, input)).catch((error) => {
|
|
3874
|
+
throw toPullRequestError("activity", error, project.api.kind, project.host);
|
|
3875
|
+
});
|
|
3876
|
+
return {
|
|
3877
|
+
...activity2.author === void 0 ? {} : { author: activity2.author },
|
|
3878
|
+
...activity2.reviewers === void 0 ? {} : { reviewers: activity2.reviewers },
|
|
3879
|
+
comments: activity2.comments,
|
|
3880
|
+
commentCount: activity2.commentCount,
|
|
3881
|
+
commentsTruncated: activity2.commentsTruncated,
|
|
3882
|
+
reviewThreads: activity2.reviewThreads,
|
|
3883
|
+
commits: activity2.commits,
|
|
3884
|
+
...activity2.reactions === void 0 ? {} : { reactions: activity2.reactions }
|
|
3885
|
+
};
|
|
3886
|
+
};
|
|
3887
|
+
const threadComments = async (input) => {
|
|
3888
|
+
const project = await requireProject(input);
|
|
3889
|
+
const read = project.api.getReviewThreadComments;
|
|
3890
|
+
if (read === void 0) {
|
|
3891
|
+
throw new PullRequestOperationFailed({
|
|
3892
|
+
operation: "threadComments",
|
|
3893
|
+
detail: "This host does not page review thread comments."
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
return read({
|
|
3897
|
+
...changeRequestRef(project, input),
|
|
3898
|
+
threadId: input.threadId,
|
|
3899
|
+
cursor: input.cursor
|
|
3900
|
+
}).catch((error) => {
|
|
3901
|
+
throw toPullRequestError("threadComments", error, project.api.kind, project.host);
|
|
3902
|
+
});
|
|
3903
|
+
};
|
|
3904
|
+
const diffUncached = async (input) => {
|
|
3905
|
+
const project = await requireProject(input);
|
|
3906
|
+
if (!project.api.capabilities.diff) {
|
|
3907
|
+
throw new PullRequestOperationFailed({
|
|
3908
|
+
operation: "diff",
|
|
3909
|
+
detail: "This host cannot provide a diff for a change request."
|
|
3910
|
+
});
|
|
3911
|
+
}
|
|
3912
|
+
return project.api.getDiff({
|
|
3913
|
+
...changeRequestRef(project, input),
|
|
3914
|
+
...input.cursor === void 0 ? {} : { cursor: input.cursor },
|
|
3915
|
+
...input.commit === void 0 ? {} : { commit: input.commit }
|
|
3916
|
+
}).catch((error) => {
|
|
3917
|
+
throw toPullRequestError("diff", error, project.api.kind, project.host);
|
|
3918
|
+
});
|
|
3919
|
+
};
|
|
3920
|
+
const diffFileContents = async (input) => {
|
|
3921
|
+
const project = await requireProject(input);
|
|
3922
|
+
const read = project.api.getDiffFileContents;
|
|
3923
|
+
if (!project.api.capabilities.diff || read === void 0) {
|
|
3924
|
+
throw new PullRequestOperationFailed({
|
|
3925
|
+
operation: "diffFileContents",
|
|
3926
|
+
detail: "This host cannot expand unchanged pull request lines."
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
return read({
|
|
3930
|
+
...changeRequestRef(project, input),
|
|
3931
|
+
...input.commit === void 0 ? {} : { commit: input.commit },
|
|
3932
|
+
changeType: input.changeType,
|
|
3933
|
+
oldPath: input.oldPath,
|
|
3934
|
+
newPath: input.newPath
|
|
3935
|
+
}).catch((error) => {
|
|
3936
|
+
throw toPullRequestError("diffFileContents", error, project.api.kind, project.host);
|
|
3937
|
+
});
|
|
3938
|
+
};
|
|
3939
|
+
const listStatsUncached = async (input) => {
|
|
3940
|
+
if (input.refs.length === 0) return { stats: [] };
|
|
3941
|
+
const { supported } = await workspace({});
|
|
3942
|
+
const byProject = new Map(supported.map((project) => [project.projectId, project]));
|
|
3943
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
3944
|
+
for (const ref of input.refs) {
|
|
3945
|
+
const project = byProject.get(ref.projectId);
|
|
3946
|
+
if (project === void 0 || project.api.listChangeRequestStats === void 0 || project.repository.toLowerCase() !== ref.repository.trim().toLowerCase()) {
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
wanted.set(`${project.projectId} ${ref.number}`, { project, number: ref.number });
|
|
3950
|
+
}
|
|
3951
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
3952
|
+
for (const entry of wanted.values()) {
|
|
3953
|
+
const held = byHost.get(entry.project.host);
|
|
3954
|
+
if (held === void 0) byHost.set(entry.project.host, [entry]);
|
|
3955
|
+
else held.push(entry);
|
|
3956
|
+
}
|
|
3957
|
+
const stats = await mapConcurrent(
|
|
3958
|
+
[...byHost.values()],
|
|
3959
|
+
REPOSITORY_CONCURRENCY,
|
|
3960
|
+
async (entries) => {
|
|
3961
|
+
const first = entries[0];
|
|
3962
|
+
const readStats = first.project.api.listChangeRequestStats;
|
|
3963
|
+
if (readStats === void 0) return [];
|
|
3964
|
+
const projectsByRepository = new Map(
|
|
3965
|
+
entries.map((entry) => [
|
|
3966
|
+
`${entry.project.repository.toLowerCase()} ${entry.number}`,
|
|
3967
|
+
entry.project
|
|
3968
|
+
])
|
|
3969
|
+
);
|
|
3970
|
+
try {
|
|
3971
|
+
const read = await readStats({
|
|
3972
|
+
cwd: first.project.workspaceRoot,
|
|
3973
|
+
host: first.project.host,
|
|
3974
|
+
changeRequests: entries.map((entry) => ({
|
|
3975
|
+
repository: entry.project.repository,
|
|
3976
|
+
number: entry.number
|
|
3977
|
+
}))
|
|
3978
|
+
});
|
|
3979
|
+
return read.flatMap((stat) => {
|
|
3980
|
+
const project = projectsByRepository.get(
|
|
3981
|
+
`${stat.repository.toLowerCase()} ${stat.number}`
|
|
3982
|
+
);
|
|
3983
|
+
return project === void 0 ? [] : [
|
|
3984
|
+
{
|
|
3985
|
+
projectId: project.projectId,
|
|
3986
|
+
repository: project.repository,
|
|
3987
|
+
number: stat.number,
|
|
3988
|
+
additions: stat.additions,
|
|
3989
|
+
deletions: stat.deletions
|
|
3990
|
+
}
|
|
3991
|
+
];
|
|
3992
|
+
});
|
|
3993
|
+
} catch {
|
|
3994
|
+
return [];
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
);
|
|
3998
|
+
return { stats: stats.flat() };
|
|
3999
|
+
};
|
|
4000
|
+
let epochCounter = 0;
|
|
4001
|
+
let listingsEpoch = 0;
|
|
4002
|
+
const refEpochs = /* @__PURE__ */ new Map();
|
|
4003
|
+
const refScope = (ref) => `${ref.projectId} ${ref.repository} ${ref.number}`;
|
|
4004
|
+
const refEpoch = (ref) => refEpochs.get(refScope(ref)) ?? 0;
|
|
4005
|
+
const bumpRefEpoch = (ref) => {
|
|
4006
|
+
const scope = refScope(ref);
|
|
4007
|
+
if (!refEpochs.has(scope) && refEpochs.size >= REF_EPOCH_CAPACITY) {
|
|
4008
|
+
const oldest = refEpochs.keys().next().value;
|
|
4009
|
+
if (oldest !== void 0) refEpochs.delete(oldest);
|
|
4010
|
+
}
|
|
4011
|
+
refEpochs.set(scope, ++epochCounter);
|
|
4012
|
+
};
|
|
4013
|
+
const listCache = createCache({
|
|
4014
|
+
capacity: LIST_CACHE_CAPACITY,
|
|
4015
|
+
ttlMs: () => LIST_CACHE_TTL_MS,
|
|
4016
|
+
now
|
|
4017
|
+
});
|
|
4018
|
+
const detailCache = createCache({
|
|
4019
|
+
capacity: DETAIL_CACHE_CAPACITY,
|
|
4020
|
+
ttlMs: () => DETAIL_CACHE_TTL_MS,
|
|
4021
|
+
now
|
|
4022
|
+
});
|
|
4023
|
+
const activityCache = createCache({
|
|
4024
|
+
capacity: DETAIL_CACHE_CAPACITY,
|
|
4025
|
+
ttlMs: () => DETAIL_CACHE_TTL_MS,
|
|
4026
|
+
now
|
|
4027
|
+
});
|
|
4028
|
+
const diffCache = createCache({
|
|
4029
|
+
capacity: DIFF_CACHE_CAPACITY,
|
|
4030
|
+
// commit 的 diff 内容寻址,活得比整份 diff 久。
|
|
4031
|
+
ttlMs: (key) => JSON.parse(key)[5] === null ? DIFF_CACHE_TTL_MS : COMMIT_DIFF_CACHE_TTL_MS,
|
|
4032
|
+
now
|
|
4033
|
+
});
|
|
4034
|
+
const listStatsCache = createCache({
|
|
4035
|
+
capacity: LIST_STATS_CACHE_CAPACITY,
|
|
4036
|
+
ttlMs: () => LIST_STATS_CACHE_TTL_MS,
|
|
4037
|
+
now
|
|
4038
|
+
});
|
|
4039
|
+
const staleDiff = createStaleWindow({
|
|
4040
|
+
capacity: DIFF_CACHE_CAPACITY,
|
|
4041
|
+
staleMs: DIFF_STALE_WINDOW_MS,
|
|
4042
|
+
now,
|
|
4043
|
+
...options.onRefreshFailure === void 0 ? {} : { onRefreshFailure: options.onRefreshFailure }
|
|
4044
|
+
});
|
|
4045
|
+
const refKey = (ref) => JSON.stringify([refEpoch(ref), ref.projectId, ref.repository, ref.number]);
|
|
4046
|
+
const list = (input) => {
|
|
4047
|
+
const key = JSON.stringify([
|
|
4048
|
+
listingsEpoch,
|
|
4049
|
+
input.state,
|
|
4050
|
+
input.involvement ?? null,
|
|
4051
|
+
// 按位置写,所以同一组筛选无论记录怎么拼出来都是同一个键。
|
|
4052
|
+
input.filters === void 0 ? null : [
|
|
4053
|
+
input.filters.draft ?? null,
|
|
4054
|
+
input.filters.review ?? null,
|
|
4055
|
+
input.filters.checks ?? null,
|
|
4056
|
+
input.filters.author ?? null,
|
|
4057
|
+
input.filters.labels ?? null,
|
|
4058
|
+
input.filters.excludedLabels ?? null
|
|
4059
|
+
],
|
|
4060
|
+
input.projectId ?? null,
|
|
4061
|
+
input.projectIds === void 0 ? null : [...input.projectIds].sort(),
|
|
4062
|
+
input.host ?? null,
|
|
4063
|
+
input.limit ?? null,
|
|
4064
|
+
input.query ?? null,
|
|
4065
|
+
input.cursors === void 0 ? null : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right))
|
|
4066
|
+
]);
|
|
4067
|
+
return listCache.get(key, () => listUncached(input));
|
|
4068
|
+
};
|
|
4069
|
+
const listStats = (input) => {
|
|
4070
|
+
if (input.refs.length === 0) return Promise.resolve({ stats: [] });
|
|
4071
|
+
const key = JSON.stringify([
|
|
4072
|
+
listingsEpoch,
|
|
4073
|
+
input.refs.map((ref) => [ref.projectId, ref.repository, ref.number]).toSorted(
|
|
4074
|
+
(left, right) => `${left[0]} ${left[1]} ${left[2]}`.localeCompare(`${right[0]} ${right[1]} ${right[2]}`)
|
|
4075
|
+
)
|
|
4076
|
+
]);
|
|
4077
|
+
return listStatsCache.get(key, () => listStatsUncached(input));
|
|
4078
|
+
};
|
|
4079
|
+
const detail = (input) => detailCache.get(refKey(input), () => detailUncached(input));
|
|
4080
|
+
const activity = (input) => activityCache.get(refKey(input), () => activityUncached(input));
|
|
4081
|
+
const diff = (input) => {
|
|
4082
|
+
const key = JSON.stringify([
|
|
4083
|
+
refEpoch(input),
|
|
4084
|
+
input.projectId,
|
|
4085
|
+
input.repository,
|
|
4086
|
+
input.number,
|
|
4087
|
+
input.cursor ?? null,
|
|
4088
|
+
input.commit ?? null
|
|
4089
|
+
]);
|
|
4090
|
+
return staleDiff.get(key, () => diffCache.get(key, () => diffUncached(input)));
|
|
4091
|
+
};
|
|
4092
|
+
const invalidate = (input) => {
|
|
4093
|
+
if (input.reference === void 0) {
|
|
4094
|
+
listingsEpoch = ++epochCounter;
|
|
4095
|
+
viewersByHost.clear();
|
|
4096
|
+
return Promise.resolve(null);
|
|
4097
|
+
}
|
|
4098
|
+
bumpRefEpoch(input.reference);
|
|
4099
|
+
return Promise.resolve(null);
|
|
4100
|
+
};
|
|
4101
|
+
const afterMutation = (ref) => {
|
|
4102
|
+
bumpRefEpoch(ref);
|
|
4103
|
+
listingsEpoch = ++epochCounter;
|
|
4104
|
+
return null;
|
|
4105
|
+
};
|
|
4106
|
+
const reviewerCandidates = async (input) => {
|
|
4107
|
+
const project = await requireProject(input);
|
|
4108
|
+
if (!project.api.capabilities.reviewers.listCandidates) {
|
|
4109
|
+
throw new PullRequestOperationFailed({
|
|
4110
|
+
operation: "reviewerCandidates",
|
|
4111
|
+
detail: "This host cannot say who may review a change request."
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
const viewer = await viewerPermissionsOf(project, input, "reviewerCandidates");
|
|
4115
|
+
if (!viewer.requestReviewers) {
|
|
4116
|
+
throw new PullRequestOperationFailed({
|
|
4117
|
+
operation: "reviewerCandidates",
|
|
4118
|
+
detail: REVIEWER_REQUEST_REFUSAL
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
return project.api.listReviewerCandidates(changeRequestRef(project, input)).catch((error) => {
|
|
4122
|
+
throw toPullRequestError("reviewerCandidates", error, project.api.kind, project.host);
|
|
4123
|
+
});
|
|
4124
|
+
};
|
|
4125
|
+
const runAction3 = async (input) => {
|
|
4126
|
+
const project = await requireProject(input);
|
|
4127
|
+
if (!project.api.capabilities.actions.includes(input.action)) {
|
|
4128
|
+
throw new PullRequestOperationFailed({
|
|
4129
|
+
operation: "runAction",
|
|
4130
|
+
detail: `This host cannot ${input.action} a change request.`
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
if (input.mergeMethod !== void 0 && !project.api.capabilities.mergeMethods.includes(input.mergeMethod)) {
|
|
4134
|
+
throw new PullRequestOperationFailed({
|
|
4135
|
+
operation: "runAction",
|
|
4136
|
+
detail: `This host cannot merge with the ${input.mergeMethod} strategy.`
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
if (input.updateMethod !== void 0 && !(project.api.capabilities.updateMethods ?? []).includes(input.updateMethod)) {
|
|
4140
|
+
throw new PullRequestOperationFailed({
|
|
4141
|
+
operation: "runAction",
|
|
4142
|
+
detail: `This host cannot update a branch by ${input.updateMethod}.`
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4145
|
+
const viewer = await viewerPermissionsOf(project, input, "runAction");
|
|
4146
|
+
if (!viewer.actions.includes(input.action)) {
|
|
4147
|
+
throw new PullRequestOperationFailed({
|
|
4148
|
+
operation: "runAction",
|
|
4149
|
+
detail: ACTION_ACCESS_REFUSALS[input.action]
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
if (input.updateMethod !== void 0 && !(viewer.updateMethods ?? []).includes(input.updateMethod)) {
|
|
4153
|
+
throw new PullRequestOperationFailed({
|
|
4154
|
+
operation: "runAction",
|
|
4155
|
+
detail: ACTION_ACCESS_REFUSALS["update-branch"]
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
await project.api.runAction({
|
|
4159
|
+
...changeRequestRef(project, input),
|
|
4160
|
+
action: input.action,
|
|
4161
|
+
...input.mergeMethod === void 0 ? {} : { mergeMethod: input.mergeMethod },
|
|
4162
|
+
...input.updateMethod === void 0 ? {} : { updateMethod: input.updateMethod }
|
|
4163
|
+
}).catch((error) => {
|
|
4164
|
+
throw toPullRequestError("runAction", error, project.api.kind, project.host);
|
|
4165
|
+
});
|
|
4166
|
+
if (input.action === "merge") {
|
|
4167
|
+
options.onMerged?.({
|
|
4168
|
+
projectId: input.projectId,
|
|
4169
|
+
repository: project.repository,
|
|
4170
|
+
number: input.number,
|
|
4171
|
+
host: project.host
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
return afterMutation(input);
|
|
4175
|
+
};
|
|
4176
|
+
const update = async (input) => {
|
|
4177
|
+
const project = await requireProject(input);
|
|
4178
|
+
const rewrite = project.api.updateChangeRequest;
|
|
4179
|
+
if (project.api.capabilities.edit?.changeRequest !== true || rewrite === void 0) {
|
|
4180
|
+
throw new PullRequestOperationFailed({
|
|
4181
|
+
operation: "update",
|
|
4182
|
+
detail: "This host cannot rewrite a change request."
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
if (input.title === void 0 && input.body === void 0) {
|
|
4186
|
+
throw new PullRequestOperationFailed({ operation: "update", detail: "Nothing was changed." });
|
|
4187
|
+
}
|
|
4188
|
+
await rewrite({
|
|
4189
|
+
...changeRequestRef(project, input),
|
|
4190
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
4191
|
+
...input.body === void 0 ? {} : { body: input.body }
|
|
4192
|
+
}).catch((error) => {
|
|
4193
|
+
throw toPullRequestError("update", error, project.api.kind, project.host);
|
|
4194
|
+
});
|
|
4195
|
+
return afterMutation(input);
|
|
4196
|
+
};
|
|
4197
|
+
const requestReviewers = async (input) => {
|
|
4198
|
+
const project = await requireProject(input);
|
|
4199
|
+
if (!project.api.capabilities.reviewers.request) {
|
|
4200
|
+
throw new PullRequestOperationFailed({
|
|
4201
|
+
operation: "requestReviewers",
|
|
4202
|
+
detail: "This host cannot ask somebody for a review."
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
const viewer = await viewerPermissionsOf(project, input, "requestReviewers");
|
|
4206
|
+
if (!viewer.requestReviewers) {
|
|
4207
|
+
throw new PullRequestOperationFailed({
|
|
4208
|
+
operation: "requestReviewers",
|
|
4209
|
+
detail: REVIEWER_REQUEST_REFUSAL
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
await project.api.setReviewerRequest({
|
|
4213
|
+
...changeRequestRef(project, input),
|
|
4214
|
+
reviewers: input.reviewers,
|
|
4215
|
+
requested: input.requested
|
|
4216
|
+
}).catch((error) => {
|
|
4217
|
+
throw toPullRequestError("requestReviewers", error, project.api.kind, project.host);
|
|
4218
|
+
});
|
|
4219
|
+
return afterMutation(input);
|
|
4220
|
+
};
|
|
4221
|
+
const comment3 = async (input) => {
|
|
4222
|
+
if (input.body.trim().length === 0) {
|
|
4223
|
+
throw new PullRequestOperationFailed({
|
|
4224
|
+
operation: "comment",
|
|
4225
|
+
detail: "A comment cannot be empty."
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
const project = await requireProject(input);
|
|
4229
|
+
if (!project.api.capabilities.comment) {
|
|
4230
|
+
throw new PullRequestOperationFailed({
|
|
4231
|
+
operation: "comment",
|
|
4232
|
+
detail: "This host cannot post a comment on a change request."
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
const viewer = await viewerPermissionsOf(project, input, "comment");
|
|
4236
|
+
if (!viewer.comment) {
|
|
4237
|
+
throw new PullRequestOperationFailed({
|
|
4238
|
+
operation: "comment",
|
|
4239
|
+
detail: "You need write access on this repository to comment on a change request."
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
await project.api.comment({ ...changeRequestRef(project, input), body: input.body }).catch((error) => {
|
|
4243
|
+
throw toPullRequestError("comment", error, project.api.kind, project.host);
|
|
4244
|
+
});
|
|
4245
|
+
return afterMutation(input);
|
|
4246
|
+
};
|
|
4247
|
+
const updateComment3 = async (input) => {
|
|
4248
|
+
if (input.body.trim().length === 0) {
|
|
4249
|
+
throw new PullRequestOperationFailed({
|
|
4250
|
+
operation: "updateComment",
|
|
4251
|
+
detail: "A comment cannot be empty."
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
const project = await requireProject(input);
|
|
4255
|
+
const rewrite = project.api.updateComment;
|
|
4256
|
+
if (project.api.capabilities.edit?.comment !== true || rewrite === void 0) {
|
|
4257
|
+
throw new PullRequestOperationFailed({
|
|
4258
|
+
operation: "updateComment",
|
|
4259
|
+
detail: "This host cannot rewrite a comment."
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
await rewrite({
|
|
4263
|
+
...changeRequestRef(project, input),
|
|
4264
|
+
commentId: input.commentId,
|
|
4265
|
+
kind: input.kind,
|
|
4266
|
+
body: input.body
|
|
4267
|
+
}).catch((error) => {
|
|
4268
|
+
throw toPullRequestError("updateComment", error, project.api.kind, project.host);
|
|
4269
|
+
});
|
|
4270
|
+
return afterMutation(input);
|
|
4271
|
+
};
|
|
4272
|
+
const submitReview3 = async (input) => {
|
|
4273
|
+
const project = await requireProject(input);
|
|
4274
|
+
const review = project.api.capabilities.review;
|
|
4275
|
+
const refuse = (detail2) => {
|
|
4276
|
+
throw new PullRequestOperationFailed({ operation: "submitReview", detail: detail2 });
|
|
4277
|
+
};
|
|
4278
|
+
if (!review.verdicts.includes(input.verdict)) {
|
|
4279
|
+
refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
|
|
4280
|
+
}
|
|
4281
|
+
if (input.comments.length > 0 && !review.inlineComment) {
|
|
4282
|
+
refuse("This host cannot comment on a line of a change request.");
|
|
4283
|
+
}
|
|
4284
|
+
if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) {
|
|
4285
|
+
refuse("A review needs a summary or at least one comment.");
|
|
4286
|
+
}
|
|
4287
|
+
const viewer = await viewerPermissionsOf(project, input, "submitReview");
|
|
4288
|
+
if (!viewer.verdicts.includes(input.verdict)) {
|
|
4289
|
+
refuse(
|
|
4290
|
+
`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
if (input.comments.length > 0 && !viewer.comment) {
|
|
4294
|
+
refuse("You need write access on this repository to comment on a line of a change request.");
|
|
4295
|
+
}
|
|
4296
|
+
await project.api.submitReview({
|
|
4297
|
+
...changeRequestRef(project, input),
|
|
4298
|
+
verdict: input.verdict,
|
|
4299
|
+
body: input.body,
|
|
4300
|
+
comments: input.comments
|
|
4301
|
+
}).catch((error) => {
|
|
4302
|
+
throw toPullRequestError("submitReview", error, project.api.kind, project.host);
|
|
4303
|
+
});
|
|
4304
|
+
return afterMutation(input);
|
|
4305
|
+
};
|
|
4306
|
+
const replyToThread3 = async (input) => {
|
|
4307
|
+
if (input.body.trim().length === 0) {
|
|
4308
|
+
throw new PullRequestOperationFailed({
|
|
4309
|
+
operation: "replyToThread",
|
|
4310
|
+
detail: "A reply cannot be empty."
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
const project = await requireProject(input);
|
|
4314
|
+
if (!project.api.capabilities.review.reply) {
|
|
4315
|
+
throw new PullRequestOperationFailed({
|
|
4316
|
+
operation: "replyToThread",
|
|
4317
|
+
detail: "This host cannot reply to a review conversation."
|
|
4318
|
+
});
|
|
4319
|
+
}
|
|
4320
|
+
const viewer = await viewerPermissionsOf(project, input, "replyToThread");
|
|
4321
|
+
if (!viewer.comment) {
|
|
4322
|
+
throw new PullRequestOperationFailed({
|
|
4323
|
+
operation: "replyToThread",
|
|
4324
|
+
detail: "You need write access on this repository to reply to a review conversation."
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
await project.api.replyToThread({
|
|
4328
|
+
...changeRequestRef(project, input),
|
|
4329
|
+
threadId: input.threadId,
|
|
4330
|
+
body: input.body
|
|
4331
|
+
}).catch((error) => {
|
|
4332
|
+
throw toPullRequestError("replyToThread", error, project.api.kind, project.host);
|
|
4333
|
+
});
|
|
4334
|
+
return afterMutation(input);
|
|
4335
|
+
};
|
|
4336
|
+
const setThreadResolution3 = async (input) => {
|
|
4337
|
+
const project = await requireProject(input);
|
|
4338
|
+
if (!project.api.capabilities.review.resolve) {
|
|
4339
|
+
throw new PullRequestOperationFailed({
|
|
4340
|
+
operation: "setThreadResolution",
|
|
4341
|
+
detail: "This host cannot resolve a review conversation."
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
const viewer = await viewerPermissionsOf(project, input, "setThreadResolution");
|
|
4345
|
+
if (!viewer.resolve) {
|
|
4346
|
+
throw new PullRequestOperationFailed({
|
|
4347
|
+
operation: "setThreadResolution",
|
|
4348
|
+
detail: "You need write access on this repository, or to have opened this change request, to resolve a review conversation."
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
await project.api.setThreadResolution({
|
|
4352
|
+
...changeRequestRef(project, input),
|
|
4353
|
+
threadId: input.threadId,
|
|
4354
|
+
resolved: input.resolved
|
|
4355
|
+
}).catch((error) => {
|
|
4356
|
+
throw toPullRequestError("setThreadResolution", error, project.api.kind, project.host);
|
|
4357
|
+
});
|
|
4358
|
+
return afterMutation(input);
|
|
4359
|
+
};
|
|
4360
|
+
const setReaction3 = async (input) => {
|
|
4361
|
+
const project = await requireProject(input);
|
|
4362
|
+
if (project.api.capabilities.reactions !== true) {
|
|
4363
|
+
throw new PullRequestOperationFailed({
|
|
4364
|
+
operation: "setReaction",
|
|
4365
|
+
detail: "This host has no reactions."
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
await project.api.setReaction({
|
|
4369
|
+
...changeRequestRef(project, input),
|
|
4370
|
+
...input.subjectId === void 0 ? {} : { subjectId: input.subjectId },
|
|
4371
|
+
content: input.content,
|
|
4372
|
+
reacted: input.reacted
|
|
4373
|
+
}).catch((error) => {
|
|
4374
|
+
throw toPullRequestError("setReaction", error, project.api.kind, project.host);
|
|
4375
|
+
});
|
|
4376
|
+
return afterMutation(input);
|
|
4377
|
+
};
|
|
4378
|
+
const act = (input) => {
|
|
4379
|
+
switch (input.op) {
|
|
4380
|
+
case "runAction":
|
|
4381
|
+
return runAction3(input);
|
|
4382
|
+
case "update":
|
|
4383
|
+
return update(input);
|
|
4384
|
+
case "requestReviewers":
|
|
4385
|
+
return requestReviewers(input);
|
|
4386
|
+
}
|
|
4387
|
+
};
|
|
4388
|
+
const converse = (input) => {
|
|
4389
|
+
switch (input.op) {
|
|
4390
|
+
case "comment":
|
|
4391
|
+
return comment3(input);
|
|
4392
|
+
case "updateComment":
|
|
4393
|
+
return updateComment3(input);
|
|
4394
|
+
case "submitReview":
|
|
4395
|
+
return submitReview3(input);
|
|
4396
|
+
case "replyToThread":
|
|
4397
|
+
return replyToThread3(input);
|
|
4398
|
+
case "setThreadResolution":
|
|
4399
|
+
return setThreadResolution3(input);
|
|
4400
|
+
case "setReaction":
|
|
4401
|
+
return setReaction3(input);
|
|
4402
|
+
}
|
|
4403
|
+
};
|
|
4404
|
+
return {
|
|
4405
|
+
list,
|
|
4406
|
+
listStats,
|
|
4407
|
+
detail,
|
|
4408
|
+
activity,
|
|
4409
|
+
threadComments,
|
|
4410
|
+
diff,
|
|
4411
|
+
diffFileContents,
|
|
4412
|
+
reviewerCandidates,
|
|
4413
|
+
invalidate,
|
|
4414
|
+
act,
|
|
4415
|
+
converse
|
|
4416
|
+
};
|
|
4417
|
+
};
|
|
4418
|
+
|
|
4419
|
+
// src/index.ts
|
|
4420
|
+
var as = (payload) => payload;
|
|
4421
|
+
var DIFF_CHUNK_CHARS = 256 * 1024;
|
|
4422
|
+
var DIFF_STREAM_ITEM_SCHEMA = {
|
|
4423
|
+
oneOf: [
|
|
4424
|
+
{
|
|
4425
|
+
type: "object",
|
|
4426
|
+
additionalProperties: false,
|
|
4427
|
+
required: ["kind", "text"],
|
|
4428
|
+
properties: { kind: { const: "chunk" }, text: { type: "string" } }
|
|
4429
|
+
},
|
|
4430
|
+
{
|
|
4431
|
+
type: "object",
|
|
4432
|
+
additionalProperties: false,
|
|
4433
|
+
required: ["kind", "truncated", "nextCursor"],
|
|
4434
|
+
properties: {
|
|
4435
|
+
kind: { const: "done" },
|
|
4436
|
+
truncated: { type: "boolean" },
|
|
4437
|
+
nextCursor: { type: ["string", "null"], minLength: 1 },
|
|
4438
|
+
omittedFileStats: {
|
|
4439
|
+
type: "array",
|
|
4440
|
+
items: {
|
|
4441
|
+
type: "object",
|
|
4442
|
+
additionalProperties: false,
|
|
4443
|
+
required: ["path", "additions", "deletions"],
|
|
4444
|
+
properties: {
|
|
4445
|
+
path: { type: "string", minLength: 1 },
|
|
4446
|
+
additions: { type: "number" },
|
|
4447
|
+
deletions: { type: "number" }
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
]
|
|
4454
|
+
};
|
|
4455
|
+
var index_default = definePlugin({
|
|
4456
|
+
id: "pull-requests",
|
|
4457
|
+
apiVersion: 2,
|
|
4458
|
+
activate: (ctx) => {
|
|
4459
|
+
const service = createPullRequestService({
|
|
4460
|
+
listProjects: () => ctx.projections.listProjects(),
|
|
4461
|
+
apis: [gitlabApi(ctx), githubApi(ctx)],
|
|
4462
|
+
onRefreshFailure: (error) => ctx.log.warn("stale diff refresh failed", { detail: String(error) }),
|
|
4463
|
+
// 编排事件流里没有「合并了」这个转移,所以这条触发事件只能从这里报。不 await:
|
|
4464
|
+
// 合并已经在主机上发生了,报不出去不该让调用方以为它没发生。
|
|
4465
|
+
onMerged: (merged) => {
|
|
4466
|
+
void ctx.events.emit("pull-request.merged", {
|
|
4467
|
+
projectId: merged.projectId,
|
|
4468
|
+
summary: `${merged.repository} change request ${merged.number} merged on ${merged.host}.`,
|
|
4469
|
+
data: {
|
|
4470
|
+
repository: merged.repository,
|
|
4471
|
+
number: merged.number,
|
|
4472
|
+
host: merged.host
|
|
4473
|
+
}
|
|
4474
|
+
}).catch((error) => {
|
|
4475
|
+
ctx.log.warn("the merged event could not be emitted", { detail: String(error) });
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
});
|
|
4479
|
+
const page = (name, description, scope, payloadContract, resultContract, handle) => {
|
|
4480
|
+
ctx.command({
|
|
4481
|
+
name,
|
|
4482
|
+
description,
|
|
4483
|
+
scope,
|
|
4484
|
+
payloadContract,
|
|
4485
|
+
...resultContract === null ? {} : { resultContract },
|
|
4486
|
+
handle
|
|
4487
|
+
});
|
|
4488
|
+
};
|
|
4489
|
+
page(
|
|
4490
|
+
"list",
|
|
4491
|
+
"Every change request the workspace's repositories have, newest update first.",
|
|
4492
|
+
"read",
|
|
4493
|
+
"PullRequestListInput",
|
|
4494
|
+
"PullRequestListResult",
|
|
4495
|
+
(payload) => service.list(as(payload))
|
|
4496
|
+
);
|
|
4497
|
+
page(
|
|
4498
|
+
"listStats",
|
|
4499
|
+
"The line counts the listing left out, for rows already on the page.",
|
|
4500
|
+
"read",
|
|
4501
|
+
"PullRequestListStatsInput",
|
|
4502
|
+
"PullRequestListStatsResult",
|
|
4503
|
+
(payload) => service.listStats(as(payload))
|
|
4504
|
+
);
|
|
4505
|
+
page(
|
|
4506
|
+
"detail",
|
|
4507
|
+
"One change request with the repository's merge strategies and this viewer's permissions.",
|
|
4508
|
+
"read",
|
|
4509
|
+
"PullRequestRef",
|
|
4510
|
+
"PullRequestDetail",
|
|
4511
|
+
(payload) => service.detail(as(payload))
|
|
4512
|
+
);
|
|
4513
|
+
page(
|
|
4514
|
+
"activity",
|
|
4515
|
+
"Comments, line threads, commits and reactions, read apart from the detail.",
|
|
4516
|
+
"read",
|
|
4517
|
+
"PullRequestRef",
|
|
4518
|
+
"PullRequestActivity",
|
|
4519
|
+
(payload) => service.activity(as(payload))
|
|
4520
|
+
);
|
|
4521
|
+
page(
|
|
4522
|
+
"threadComments",
|
|
4523
|
+
"The rest of one review thread, after a reader asks to carry on reading it.",
|
|
4524
|
+
"read",
|
|
4525
|
+
"PullRequestThreadCommentsInput",
|
|
4526
|
+
"PullRequestThreadCommentsResult",
|
|
4527
|
+
(payload) => service.threadComments(as(payload))
|
|
4528
|
+
);
|
|
4529
|
+
ctx.stream({
|
|
4530
|
+
name: "diff",
|
|
4531
|
+
description: "One slice of the change request patch, or of one commit of it.",
|
|
4532
|
+
scope: "read",
|
|
4533
|
+
payloadContract: "PullRequestDiffInput",
|
|
4534
|
+
itemSchema: DIFF_STREAM_ITEM_SCHEMA,
|
|
4535
|
+
handle: async (payload, emit, signal) => {
|
|
4536
|
+
const slice = await service.diff(as(payload));
|
|
4537
|
+
for (let offset = 0; offset < slice.patch.length; offset += DIFF_CHUNK_CHARS) {
|
|
4538
|
+
if (signal.aborted) return;
|
|
4539
|
+
emit({ kind: "chunk", text: slice.patch.slice(offset, offset + DIFF_CHUNK_CHARS) });
|
|
4540
|
+
}
|
|
4541
|
+
if (signal.aborted) return;
|
|
4542
|
+
emit({
|
|
4543
|
+
kind: "done",
|
|
4544
|
+
truncated: slice.truncated,
|
|
4545
|
+
nextCursor: slice.nextCursor,
|
|
4546
|
+
...slice.omittedFileStats === void 0 ? {} : { omittedFileStats: slice.omittedFileStats }
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4549
|
+
});
|
|
4550
|
+
page(
|
|
4551
|
+
"diffText",
|
|
4552
|
+
"The whole change request patch, or of one commit of it.",
|
|
4553
|
+
"read",
|
|
4554
|
+
"PullRequestDiffInput",
|
|
4555
|
+
"PullRequestDiffResult",
|
|
4556
|
+
(payload) => service.diff(as(payload))
|
|
4557
|
+
);
|
|
4558
|
+
page(
|
|
4559
|
+
"diffFileContents",
|
|
4560
|
+
"The whole old and new file at the revisions the patch was made from.",
|
|
4561
|
+
"read",
|
|
4562
|
+
"PullRequestDiffFileContentsInput",
|
|
4563
|
+
"PullRequestDiffFileContentsResult",
|
|
4564
|
+
(payload) => service.diffFileContents(as(payload))
|
|
4565
|
+
);
|
|
4566
|
+
page(
|
|
4567
|
+
"reviewerCandidates",
|
|
4568
|
+
"The people this change request may be sent to, with those already asked marked.",
|
|
4569
|
+
"read",
|
|
4570
|
+
"PullRequestRef",
|
|
4571
|
+
"PullRequestReviewerCandidateList",
|
|
4572
|
+
(payload) => service.reviewerCandidates(as(payload))
|
|
4573
|
+
);
|
|
4574
|
+
page(
|
|
4575
|
+
"invalidate",
|
|
4576
|
+
"Forget what is cached, so the next read asks the host again.",
|
|
4577
|
+
"read",
|
|
4578
|
+
"PullRequestInvalidateInput",
|
|
4579
|
+
null,
|
|
4580
|
+
(payload) => service.invalidate(as(payload))
|
|
4581
|
+
);
|
|
4582
|
+
page(
|
|
4583
|
+
"act",
|
|
4584
|
+
"Merges, closes, rewrites, or asks for a review on a change request.",
|
|
4585
|
+
"operate",
|
|
4586
|
+
"PullRequestActRequest",
|
|
4587
|
+
null,
|
|
4588
|
+
(payload) => service.act(as(payload))
|
|
4589
|
+
);
|
|
4590
|
+
page(
|
|
4591
|
+
"converse",
|
|
4592
|
+
"Comments, reviews, replies, resolves, or reacts on a change request.",
|
|
4593
|
+
"operate",
|
|
4594
|
+
"PullRequestConverseRequest",
|
|
4595
|
+
null,
|
|
4596
|
+
(payload) => service.converse(as(payload))
|
|
4597
|
+
);
|
|
4598
|
+
ctx.command({
|
|
4599
|
+
name: "workspaceProjects",
|
|
4600
|
+
description: "Git repositories in this machine's workspace, with their remote identity.",
|
|
4601
|
+
scope: "read",
|
|
4602
|
+
payloadSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
4603
|
+
resultSchema: {
|
|
4604
|
+
type: "object",
|
|
4605
|
+
additionalProperties: false,
|
|
4606
|
+
required: ["projects"],
|
|
4607
|
+
properties: {
|
|
4608
|
+
projects: {
|
|
4609
|
+
type: "array",
|
|
4610
|
+
items: {
|
|
4611
|
+
type: "object",
|
|
4612
|
+
additionalProperties: false,
|
|
4613
|
+
required: ["projectId", "title", "workspaceRoot"],
|
|
4614
|
+
properties: {
|
|
4615
|
+
projectId: { type: "string", minLength: 1 },
|
|
4616
|
+
title: { type: "string" },
|
|
4617
|
+
workspaceRoot: { type: "string" },
|
|
4618
|
+
repositoryIdentity: {
|
|
4619
|
+
type: ["object", "null"],
|
|
4620
|
+
additionalProperties: false,
|
|
4621
|
+
properties: {
|
|
4622
|
+
provider: { type: "string" },
|
|
4623
|
+
canonicalKey: { type: "string" },
|
|
4624
|
+
remoteName: { type: "string" },
|
|
4625
|
+
remoteUrl: { type: "string" },
|
|
4626
|
+
displayName: { type: "string" },
|
|
4627
|
+
owner: { type: "string" },
|
|
4628
|
+
name: { type: "string" },
|
|
4629
|
+
rootPath: { type: "string" }
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
},
|
|
4637
|
+
handle: async () => {
|
|
4638
|
+
const projects = await ctx.projections.listProjects();
|
|
4639
|
+
return {
|
|
4640
|
+
projects: projects.map((project) => ({
|
|
4641
|
+
projectId: project.projectId,
|
|
4642
|
+
title: project.title,
|
|
4643
|
+
workspaceRoot: project.workspaceRoot,
|
|
4644
|
+
repositoryIdentity: project.repositoryIdentity
|
|
4645
|
+
}))
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
});
|
|
4649
|
+
ctx.settingsCard({
|
|
4650
|
+
title: "Pull requests",
|
|
4651
|
+
description: "Pull and merge requests are read and written straight over the host API, with a token you paste here.",
|
|
4652
|
+
fields: [
|
|
4653
|
+
{
|
|
4654
|
+
key: "gitlabHost",
|
|
4655
|
+
kind: "text",
|
|
4656
|
+
label: "GitLab host",
|
|
4657
|
+
description: "Fill in the domain of a self-managed instance.",
|
|
4658
|
+
placeholder: "gitlab.com"
|
|
4659
|
+
},
|
|
4660
|
+
{
|
|
4661
|
+
key: "gitlabToken",
|
|
4662
|
+
kind: "password",
|
|
4663
|
+
label: "GitLab token",
|
|
4664
|
+
// 不标 required:两台主机共用这一张卡,只用 GitHub 的人不该被一枚 GitLab
|
|
4665
|
+
// token 卡住。缺 token 的那一侧在命令里报 unauthenticated。
|
|
4666
|
+
description: "A personal access token with the `api` scope."
|
|
4667
|
+
},
|
|
4668
|
+
{
|
|
4669
|
+
key: "githubHost",
|
|
4670
|
+
kind: "text",
|
|
4671
|
+
label: "GitHub host",
|
|
4672
|
+
description: "Fill in the domain of a GitHub Enterprise install.",
|
|
4673
|
+
placeholder: "github.com"
|
|
4674
|
+
},
|
|
4675
|
+
{
|
|
4676
|
+
key: "githubToken",
|
|
4677
|
+
kind: "password",
|
|
4678
|
+
label: "GitHub token",
|
|
4679
|
+
description: "A personal access token that may read and write the repositories you review. The `repo` scope is enough; add `read:org` to also see team reviewers."
|
|
4680
|
+
}
|
|
4681
|
+
]
|
|
4682
|
+
});
|
|
4683
|
+
ctx.log.info("pull requests plugin active", { hosts: ["gitlab", "github"] });
|
|
4684
|
+
}
|
|
4685
|
+
});
|
|
4686
|
+
export {
|
|
4687
|
+
index_default as default
|
|
4688
|
+
};
|