@ycodium-ai/plugin-pull-requests 0.2.3094 → 0.2.3096

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.
Files changed (3) hide show
  1. package/assets/ui.js +14 -14
  2. package/dist/index.js +453 -427
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -145,35 +145,6 @@ var createStaleWindow = (options) => {
145
145
  };
146
146
  };
147
147
 
148
- // src/service/cursor.ts
149
- 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})*)?$/;
150
- var parseListCursor = (raw) => {
151
- const match = LIST_CURSOR_PATTERN.exec(raw);
152
- if (match === null) return null;
153
- const seenAt = match[3];
154
- return {
155
- updatedBefore: match[1],
156
- delivered: Number(match[2]),
157
- seenAt: seenAt === void 0 ? [] : seenAt.split(",").map(Number)
158
- };
159
- };
160
- var listCursorKey = (host, repository) => `${host} ${repository.toLowerCase()}`;
161
- var listCursorAt = (previous, boundary, fetched, deliveredCount) => {
162
- const seenAt = [
163
- ...previous?.updatedBefore === boundary ? previous.seenAt : [],
164
- ...fetched.filter((item) => item.updatedAt === boundary).map((item) => item.number)
165
- ];
166
- return `${boundary}|${(previous?.delivered ?? 0) + deliveredCount}|${seenAt.join(",")}`;
167
- };
168
- var nextListCursor = (previous, fetched, delivered, cursorAdvance = delivered.length) => {
169
- if (fetched.length === 0) return null;
170
- const oldest = fetched.reduce((left, right) => right.updatedAt < left.updatedAt ? right : left);
171
- return listCursorAt(previous, oldest.updatedAt, fetched, cursorAdvance);
172
- };
173
- var withoutAlreadySent = (cursor, items) => cursor === void 0 ? items : items.filter(
174
- (item) => item.updatedAt !== cursor.updatedBefore || !cursor.seenAt.includes(item.number)
175
- );
176
-
177
148
  // src/service/errors.ts
178
149
  var PROVIDER_REQUIREMENT = {
179
150
  github: {
@@ -235,6 +206,35 @@ var PullRequestOperationFailed = class extends Error {
235
206
  }
236
207
  };
237
208
 
209
+ // src/service/cursor.ts
210
+ 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})*)?$/;
211
+ var parseListCursor = (raw) => {
212
+ const match = LIST_CURSOR_PATTERN.exec(raw);
213
+ if (match === null) return null;
214
+ const seenAt = match[3];
215
+ return {
216
+ updatedBefore: match[1],
217
+ delivered: Number(match[2]),
218
+ seenAt: seenAt === void 0 ? [] : seenAt.split(",").map(Number)
219
+ };
220
+ };
221
+ var listCursorKey = (host, repository) => `${host} ${repository.toLowerCase()}`;
222
+ var listCursorAt = (previous, boundary, fetched, deliveredCount) => {
223
+ const seenAt = [
224
+ ...previous?.updatedBefore === boundary ? previous.seenAt : [],
225
+ ...fetched.filter((item) => item.updatedAt === boundary).map((item) => item.number)
226
+ ];
227
+ return `${boundary}|${(previous?.delivered ?? 0) + deliveredCount}|${seenAt.join(",")}`;
228
+ };
229
+ var nextListCursor = (previous, fetched, delivered, cursorAdvance = delivered.length) => {
230
+ if (fetched.length === 0) return null;
231
+ const oldest = fetched.reduce((left, right) => right.updatedAt < left.updatedAt ? right : left);
232
+ return listCursorAt(previous, oldest.updatedAt, fetched, cursorAdvance);
233
+ };
234
+ var withoutAlreadySent = (cursor, items) => cursor === void 0 ? items : items.filter(
235
+ (item) => item.updatedAt !== cursor.updatedBefore || !cursor.seenAt.includes(item.number)
236
+ );
237
+
238
238
  // src/githubHttp.ts
239
239
  var DEFAULT_TIMEOUT_MS = 3e4;
240
240
  var DEFAULT_MAX_BYTES = 1024 * 1024;
@@ -3396,124 +3396,30 @@ var withRateLimitBackoff = (api, host, backoff) => {
3396
3396
  };
3397
3397
  };
3398
3398
 
3399
- // src/service/workspace.ts
3400
- var repositoryIdentityOf = (identity) => {
3401
- if (identity.displayName) return identity.displayName;
3402
- return identity.owner && identity.name ? `${identity.owner}/${identity.name}` : null;
3403
- };
3404
- var hostOf = (identity, kind) => {
3405
- const host = identity.canonicalKey.split("/")[0]?.trim();
3406
- return host === void 0 || host.length === 0 ? kind : host.toLowerCase();
3407
- };
3408
- var remoteHostOf = (remoteUrl) => {
3409
- const normalized = remoteUrl.trim().replace(/\/+$/g, "").toLowerCase();
3410
- if (/^(?:ssh|https?|git):\/\//i.test(normalized)) {
3411
- try {
3412
- return new URL(normalized).hostname;
3413
- } catch {
3414
- return "";
3415
- }
3416
- }
3417
- return /^[a-z0-9._-]+@([^:/\s]+):/i.exec(normalized)?.[1] ?? "";
3418
- };
3419
- var claimTable = (apis) => {
3420
- const claims = /* @__PURE__ */ new Map();
3421
- for (const api of apis) {
3422
- for (const host of api.hosts) {
3423
- if (host.length > 0 && !claims.has(host)) claims.set(host, api.kind);
3424
- }
3425
- }
3426
- return claims;
3427
- };
3428
- var isArchivedWorkspaceProject = (project) => project.archivedAt != null;
3429
- var listWorkspaceProjects = async (input) => {
3430
- const projects = await input.listProjects();
3431
- const claims = claimTable(input.apis);
3432
- const byKind = new Map(input.apis.map((api) => [api.kind, api]));
3433
- const supported = [];
3434
- const unimplemented = /* @__PURE__ */ new Map();
3435
- const viewerRoots = /* @__PURE__ */ new Map();
3436
- const seen = /* @__PURE__ */ new Set();
3437
- for (const project of projects) {
3438
- if (input.filter.projectId !== void 0 && project.projectId !== input.filter.projectId) {
3439
- continue;
3440
- }
3441
- if (input.filter.projectId === void 0 && isArchivedWorkspaceProject(project)) continue;
3442
- if (input.filter.projectIds !== void 0 && !input.filter.projectIds.includes(project.projectId)) {
3443
- continue;
3444
- }
3445
- const identity = project.repositoryIdentity;
3446
- if (identity === null) continue;
3447
- const repository = repositoryIdentityOf(identity);
3448
- if (repository === null) continue;
3449
- let kind = identity.provider;
3450
- if (kind === "unknown") {
3451
- const claimed = claims.get(remoteHostOf(identity.remoteUrl));
3452
- if (claimed !== void 0) kind = claimed;
3453
- }
3454
- const host = hostOf(identity, kind);
3455
- if (input.filter.host !== void 0 && host !== input.filter.host.trim().toLowerCase()) {
3456
- continue;
3457
- }
3458
- const api = byKind.get(kind) ?? null;
3459
- if (api !== null) {
3460
- const roots = viewerRoots.get(host);
3461
- if (roots === void 0) viewerRoots.set(host, [project.workspaceRoot]);
3462
- else if (!roots.includes(project.workspaceRoot)) roots.push(project.workspaceRoot);
3463
- }
3464
- const key = listCursorKey(host, repository);
3465
- if (seen.has(key)) continue;
3466
- seen.add(key);
3467
- if (api === null) {
3468
- const counted = unimplemented.get(host);
3469
- if (counted === void 0) unimplemented.set(host, { kind, projectCount: 1 });
3470
- else counted.projectCount += 1;
3471
- continue;
3472
- }
3473
- supported.push({
3474
- projectId: project.projectId,
3475
- projectTitle: project.title,
3476
- workspaceRoot: project.workspaceRoot,
3477
- api: withRateLimitBackoff(api, host, input.backoff),
3478
- repository,
3479
- host
3399
+ // src/service/serviceSupport.ts
3400
+ var isProviderUnusable = (failure) => failure.code === "missing-tool" || failure.code === "unauthenticated";
3401
+ var unavailableReasonOf = (failure) => failure.code === "missing-tool" ? "token-missing" : "token-rejected";
3402
+ var toPullRequestError = (operation, error, kind, host) => {
3403
+ const failure = providerFailureOf(error);
3404
+ if (isProviderUnusable(failure)) {
3405
+ return new PullRequestUnavailable({
3406
+ reason: unavailableReasonOf(failure),
3407
+ provider: kind,
3408
+ ...host === void 0 ? {} : { host }
3480
3409
  });
3481
3410
  }
3482
- return { supported, unimplemented, viewerRoots };
3411
+ return new PullRequestOperationFailed({
3412
+ operation,
3413
+ detail: failure.detail,
3414
+ ...failure.retryAt === void 0 ? {} : { retryAt: failure.retryAt }
3415
+ });
3483
3416
  };
3484
3417
 
3485
- // src/service/pullRequestService.ts
3418
+ // src/service/listReads.ts
3486
3419
  var DEFAULT_REPOSITORY_LIST_LIMIT = 99;
3487
3420
  var REPOSITORY_CONCURRENCY = 12;
3488
3421
  var REPOSITORY_SEARCH_CHUNK = 100;
3489
- var LIST_CACHE_TTL_MS = 3e4;
3490
- var DETAIL_CACHE_TTL_MS = 15e3;
3491
- var DIFF_CACHE_TTL_MS = 6e4;
3492
- var COMMIT_DIFF_CACHE_TTL_MS = 10 * 6e4;
3493
- var LIST_STATS_CACHE_TTL_MS = 6e4;
3494
- var DIFF_STALE_WINDOW_MS = 10 * 6e4;
3495
3422
  var VIEWER_CACHE_TTL_MS = 10 * 6e4;
3496
- var LIST_CACHE_CAPACITY = 64;
3497
- var LIST_STATS_CACHE_CAPACITY = 32;
3498
- var DETAIL_CACHE_CAPACITY = 128;
3499
- var DIFF_CACHE_CAPACITY = 128;
3500
- var REF_EPOCH_CAPACITY = 2048;
3501
- var VERDICT_LABELS = {
3502
- comment: "review",
3503
- approve: "approve",
3504
- "request-changes": "request changes on"
3505
- };
3506
- var ACTION_ACCESS_REFUSALS = {
3507
- merge: "You need write access on this repository to merge.",
3508
- ready: "You need write access on this repository, or to have opened this change request, to mark it ready for review.",
3509
- draft: "You need write access on this repository, or to have opened this change request, to return it to a draft.",
3510
- close: "You need write access on this repository, or to have opened this change request, to close it.",
3511
- "update-branch": "You need write access on this repository, or to have opened this change request, to update its branch.",
3512
- reopen: "You need write access on this repository, or to have opened this change request, to reopen it.",
3513
- "enable-auto-merge": "You need write access on this repository to have it merged for you once it is ready.",
3514
- "disable-auto-merge": "You need write access on this repository to stop it being merged for you once it is ready."
3515
- };
3516
- var REVIEWER_REQUEST_REFUSAL = "You need write access on this repository to ask for a review.";
3517
3423
  var mapConcurrent = async (items, limit, run) => {
3518
3424
  const results = Array.from({ length: items.length });
3519
3425
  let next = 0;
@@ -3539,24 +3445,7 @@ var firstSuccess = async (runs) => {
3539
3445
  }
3540
3446
  throw last;
3541
3447
  };
3542
- var isProviderUnusable = (failure) => failure.code === "missing-tool" || failure.code === "unauthenticated";
3543
- var unavailableReasonOf = (failure) => failure.code === "missing-tool" ? "token-missing" : "token-rejected";
3544
3448
  var providerDetail = (failure, kind) => isProviderUnusable(failure) ? providerRequirement(kind, unavailableReasonOf(failure)) ?? failure.detail : failure.detail;
3545
- var toPullRequestError = (operation, error, kind, host) => {
3546
- const failure = providerFailureOf(error);
3547
- if (isProviderUnusable(failure)) {
3548
- return new PullRequestUnavailable({
3549
- reason: unavailableReasonOf(failure),
3550
- provider: kind,
3551
- ...host === void 0 ? {} : { host }
3552
- });
3553
- }
3554
- return new PullRequestOperationFailed({
3555
- operation,
3556
- detail: failure.detail,
3557
- ...failure.retryAt === void 0 ? {} : { retryAt: failure.retryAt }
3558
- });
3559
- };
3560
3449
  var resolveAuthorFilter = (author, viewer) => {
3561
3450
  const trimmed = author.trim();
3562
3451
  if (!/^@?me$/iu.test(trimmed)) return trimmed;
@@ -3570,45 +3459,8 @@ var matchesRowFilters = (item, filters, viewer) => {
3570
3459
  // 也就是 "none" 问的那个;`undefined` 是「这台主机根本不汇总」,那种行不判。
3571
3460
  (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());
3572
3461
  };
3573
- var createPullRequestService = (options) => {
3574
- const now = options.now ?? Date.now;
3575
- const backoff = createBackoff({ now });
3576
- const workspace = (filter) => listWorkspaceProjects({
3577
- listProjects: options.listProjects,
3578
- apis: options.apis,
3579
- backoff,
3580
- filter
3581
- }).catch(() => {
3582
- throw new PullRequestOperationFailed({
3583
- operation: "listProjects",
3584
- detail: "The project list could not be read."
3585
- });
3586
- });
3587
- const requireProject = async (ref) => {
3588
- const { supported } = await workspace({ projectId: ref.projectId });
3589
- const match = supported[0];
3590
- if (!match) throw new PullRequestUnavailable({ reason: "provider-unsupported" });
3591
- if (match.repository.toLowerCase() !== ref.repository.trim().toLowerCase()) {
3592
- throw new PullRequestOperationFailed({
3593
- operation: "resolveRepository",
3594
- detail: "The change request does not belong to the selected project."
3595
- });
3596
- }
3597
- return match;
3598
- };
3599
- const changeRequestRef = (project, ref) => ({
3600
- cwd: project.workspaceRoot,
3601
- repository: project.repository,
3602
- host: project.host,
3603
- number: ref.number
3604
- });
3605
- const viewerPermissionsOf = async (project, ref, operation) => {
3606
- try {
3607
- return await project.api.getViewerPermissions(changeRequestRef(project, ref));
3608
- } catch (error) {
3609
- throw toPullRequestError(operation, error, project.api.kind, project.host);
3610
- }
3611
- };
3462
+ var makePullRequestListReads = (deps) => {
3463
+ const { workspace, now, viewersByHost } = deps;
3612
3464
  const decodeCursors = (cursors) => {
3613
3465
  if (cursors === void 0) return null;
3614
3466
  const decoded = /* @__PURE__ */ new Map();
@@ -3624,7 +3476,6 @@ var createPullRequestService = (options) => {
3624
3476
  }
3625
3477
  return decoded;
3626
3478
  };
3627
- const viewersByHost = /* @__PURE__ */ new Map();
3628
3479
  const resolveViewers = (projects, viewerRoots) => mapConcurrent(
3629
3480
  [...new Set(projects.map(({ host }) => host))],
3630
3481
  REPOSITORY_CONCURRENCY,
@@ -3865,117 +3716,6 @@ ${cursorOf(project)?.updatedBefore ?? ""}`;
3865
3716
  nextCursors
3866
3717
  };
3867
3718
  };
3868
- const detailUncached = async (input) => {
3869
- const project = await requireProject(input);
3870
- const [changeRequest, viewer] = await Promise.all([
3871
- project.api.getChangeRequest(changeRequestRef(project, input)).catch((error) => {
3872
- throw toPullRequestError("detail", error, project.api.kind, project.host);
3873
- }),
3874
- viewerOf(project)
3875
- ]);
3876
- return {
3877
- provider: project.api.kind,
3878
- capabilities: project.api.capabilities,
3879
- projectId: project.projectId,
3880
- projectTitle: project.projectTitle,
3881
- workspaceRoot: project.workspaceRoot,
3882
- repository: project.repository,
3883
- number: changeRequest.number,
3884
- title: changeRequest.title,
3885
- body: changeRequest.body,
3886
- url: changeRequest.url,
3887
- author: changeRequest.author,
3888
- state: changeRequest.state,
3889
- isDraft: changeRequest.isDraft,
3890
- mergeability: changeRequest.mergeability,
3891
- additions: changeRequest.additions,
3892
- deletions: changeRequest.deletions,
3893
- changedFiles: changeRequest.changedFiles,
3894
- headBranch: changeRequest.headBranch,
3895
- baseBranch: changeRequest.baseBranch,
3896
- createdAt: changeRequest.createdAt,
3897
- updatedAt: changeRequest.updatedAt,
3898
- mergedAt: changeRequest.mergedAt,
3899
- closedAt: changeRequest.closedAt,
3900
- reviewers: changeRequest.reviewers,
3901
- labels: changeRequest.labels,
3902
- checks: changeRequest.checks,
3903
- mergeCapabilities: changeRequest.mergeCapabilities,
3904
- viewerPermissions: changeRequest.viewerPermissions,
3905
- ...viewer === null || viewer.trim().length === 0 ? {} : { viewer },
3906
- ...changeRequest.baseComparison === void 0 ? {} : { baseComparison: changeRequest.baseComparison },
3907
- ...changeRequest.behindBy === void 0 ? {} : { behindBy: changeRequest.behindBy },
3908
- ...changeRequest.autoMergeEnabled === void 0 ? {} : { autoMergeEnabled: changeRequest.autoMergeEnabled }
3909
- };
3910
- };
3911
- const activityUncached = async (input) => {
3912
- const project = await requireProject(input);
3913
- const activity2 = await project.api.getChangeRequestActivity(changeRequestRef(project, input)).catch((error) => {
3914
- throw toPullRequestError("activity", error, project.api.kind, project.host);
3915
- });
3916
- return {
3917
- ...activity2.author === void 0 ? {} : { author: activity2.author },
3918
- ...activity2.reviewers === void 0 ? {} : { reviewers: activity2.reviewers },
3919
- comments: activity2.comments,
3920
- commentCount: activity2.commentCount,
3921
- commentsTruncated: activity2.commentsTruncated,
3922
- reviewThreads: activity2.reviewThreads,
3923
- commits: activity2.commits,
3924
- ...activity2.reactions === void 0 ? {} : { reactions: activity2.reactions }
3925
- };
3926
- };
3927
- const threadComments = async (input) => {
3928
- const project = await requireProject(input);
3929
- const read = project.api.getReviewThreadComments;
3930
- if (read === void 0) {
3931
- throw new PullRequestOperationFailed({
3932
- operation: "threadComments",
3933
- detail: "This host does not page review thread comments."
3934
- });
3935
- }
3936
- return read({
3937
- ...changeRequestRef(project, input),
3938
- threadId: input.threadId,
3939
- cursor: input.cursor
3940
- }).catch((error) => {
3941
- throw toPullRequestError("threadComments", error, project.api.kind, project.host);
3942
- });
3943
- };
3944
- const diffUncached = async (input) => {
3945
- const project = await requireProject(input);
3946
- if (!project.api.capabilities.diff) {
3947
- throw new PullRequestOperationFailed({
3948
- operation: "diff",
3949
- detail: "This host cannot provide a diff for a change request."
3950
- });
3951
- }
3952
- return project.api.getDiff({
3953
- ...changeRequestRef(project, input),
3954
- ...input.cursor === void 0 ? {} : { cursor: input.cursor },
3955
- ...input.commit === void 0 ? {} : { commit: input.commit }
3956
- }).catch((error) => {
3957
- throw toPullRequestError("diff", error, project.api.kind, project.host);
3958
- });
3959
- };
3960
- const diffFileContents = async (input) => {
3961
- const project = await requireProject(input);
3962
- const read = project.api.getDiffFileContents;
3963
- if (!project.api.capabilities.diff || read === void 0) {
3964
- throw new PullRequestOperationFailed({
3965
- operation: "diffFileContents",
3966
- detail: "This host cannot expand unchanged pull request lines."
3967
- });
3968
- }
3969
- return read({
3970
- ...changeRequestRef(project, input),
3971
- ...input.commit === void 0 ? {} : { commit: input.commit },
3972
- changeType: input.changeType,
3973
- oldPath: input.oldPath,
3974
- newPath: input.newPath
3975
- }).catch((error) => {
3976
- throw toPullRequestError("diffFileContents", error, project.api.kind, project.host);
3977
- });
3978
- };
3979
3719
  const listStatsUncached = async (input) => {
3980
3720
  if (input.refs.length === 0) return { stats: [] };
3981
3721
  const { supported } = await workspace({});
@@ -4037,128 +3777,130 @@ ${cursorOf(project)?.updatedBefore ?? ""}`;
4037
3777
  );
4038
3778
  return { stats: stats.flat() };
4039
3779
  };
4040
- let epochCounter = 0;
4041
- let listingsEpoch = 0;
4042
- const refEpochs = /* @__PURE__ */ new Map();
4043
- const refScope = (ref) => `${ref.projectId} ${ref.repository} ${ref.number}`;
4044
- const refEpoch = (ref) => refEpochs.get(refScope(ref)) ?? 0;
4045
- const bumpRefEpoch = (ref) => {
4046
- const scope = refScope(ref);
4047
- if (!refEpochs.has(scope) && refEpochs.size >= REF_EPOCH_CAPACITY) {
4048
- const oldest = refEpochs.keys().next().value;
4049
- if (oldest !== void 0) refEpochs.delete(oldest);
3780
+ return { listUncached, listStatsUncached, viewerOf };
3781
+ };
3782
+
3783
+ // src/service/workspace.ts
3784
+ var repositoryIdentityOf = (identity) => {
3785
+ if (identity.displayName) return identity.displayName;
3786
+ return identity.owner && identity.name ? `${identity.owner}/${identity.name}` : null;
3787
+ };
3788
+ var hostOf = (identity, kind) => {
3789
+ const host = identity.canonicalKey.split("/")[0]?.trim();
3790
+ return host === void 0 || host.length === 0 ? kind : host.toLowerCase();
3791
+ };
3792
+ var remoteHostOf = (remoteUrl) => {
3793
+ const normalized = remoteUrl.trim().replace(/\/+$/g, "").toLowerCase();
3794
+ if (/^(?:ssh|https?|git):\/\//i.test(normalized)) {
3795
+ try {
3796
+ return new URL(normalized).hostname;
3797
+ } catch {
3798
+ return "";
4050
3799
  }
4051
- refEpochs.set(scope, ++epochCounter);
4052
- };
4053
- const listCache = createCache({
4054
- capacity: LIST_CACHE_CAPACITY,
4055
- ttlMs: () => LIST_CACHE_TTL_MS,
4056
- now
4057
- });
4058
- const detailCache = createCache({
4059
- capacity: DETAIL_CACHE_CAPACITY,
4060
- ttlMs: () => DETAIL_CACHE_TTL_MS,
4061
- now
4062
- });
4063
- const activityCache = createCache({
4064
- capacity: DETAIL_CACHE_CAPACITY,
4065
- ttlMs: () => DETAIL_CACHE_TTL_MS,
4066
- now
4067
- });
4068
- const diffCache = createCache({
4069
- capacity: DIFF_CACHE_CAPACITY,
4070
- // commit 的 diff 内容寻址,活得比整份 diff 久。
4071
- ttlMs: (key) => JSON.parse(key)[5] === null ? DIFF_CACHE_TTL_MS : COMMIT_DIFF_CACHE_TTL_MS,
4072
- now
4073
- });
4074
- const listStatsCache = createCache({
4075
- capacity: LIST_STATS_CACHE_CAPACITY,
4076
- ttlMs: () => LIST_STATS_CACHE_TTL_MS,
4077
- now
4078
- });
4079
- const staleDiff = createStaleWindow({
4080
- capacity: DIFF_CACHE_CAPACITY,
4081
- staleMs: DIFF_STALE_WINDOW_MS,
4082
- now,
4083
- ...options.onRefreshFailure === void 0 ? {} : { onRefreshFailure: options.onRefreshFailure }
4084
- });
4085
- const refKey = (ref) => JSON.stringify([refEpoch(ref), ref.projectId, ref.repository, ref.number]);
4086
- const list = (input) => {
4087
- const key = JSON.stringify([
4088
- listingsEpoch,
4089
- input.state,
4090
- input.involvement ?? null,
4091
- // 按位置写,所以同一组筛选无论记录怎么拼出来都是同一个键。
4092
- input.filters === void 0 ? null : [
4093
- input.filters.draft ?? null,
4094
- input.filters.review ?? null,
4095
- input.filters.checks ?? null,
4096
- input.filters.author ?? null,
4097
- input.filters.labels ?? null,
4098
- input.filters.excludedLabels ?? null
4099
- ],
4100
- input.projectId ?? null,
4101
- input.projectIds === void 0 ? null : [...input.projectIds].sort(),
4102
- input.host ?? null,
4103
- input.limit ?? null,
4104
- input.query ?? null,
4105
- input.cursors === void 0 ? null : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right))
4106
- ]);
4107
- return listCache.get(key, () => listUncached(input));
4108
- };
4109
- const listStats = (input) => {
4110
- if (input.refs.length === 0) return Promise.resolve({ stats: [] });
4111
- const key = JSON.stringify([
4112
- listingsEpoch,
4113
- input.refs.map((ref) => [ref.projectId, ref.repository, ref.number]).toSorted(
4114
- (left, right) => `${left[0]} ${left[1]} ${left[2]}`.localeCompare(`${right[0]} ${right[1]} ${right[2]}`)
4115
- )
4116
- ]);
4117
- return listStatsCache.get(key, () => listStatsUncached(input));
4118
- };
4119
- const detail = (input) => detailCache.get(refKey(input), () => detailUncached(input));
4120
- const activity = (input) => activityCache.get(refKey(input), () => activityUncached(input));
4121
- const diff = (input) => {
4122
- const key = JSON.stringify([
4123
- refEpoch(input),
4124
- input.projectId,
4125
- input.repository,
4126
- input.number,
4127
- input.cursor ?? null,
4128
- input.commit ?? null
4129
- ]);
4130
- return staleDiff.get(key, () => diffCache.get(key, () => diffUncached(input)));
4131
- };
4132
- const invalidate = (input) => {
4133
- if (input.reference === void 0) {
4134
- listingsEpoch = ++epochCounter;
4135
- viewersByHost.clear();
4136
- return Promise.resolve(null);
3800
+ }
3801
+ return /^[a-z0-9._-]+@([^:/\s]+):/i.exec(normalized)?.[1] ?? "";
3802
+ };
3803
+ var claimTable = (apis) => {
3804
+ const claims = /* @__PURE__ */ new Map();
3805
+ for (const api of apis) {
3806
+ for (const host of api.hosts) {
3807
+ if (host.length > 0 && !claims.has(host)) claims.set(host, api.kind);
4137
3808
  }
4138
- bumpRefEpoch(input.reference);
4139
- return Promise.resolve(null);
4140
- };
4141
- const afterMutation = (ref) => {
4142
- bumpRefEpoch(ref);
4143
- listingsEpoch = ++epochCounter;
4144
- return null;
4145
- };
4146
- const reviewerCandidates = async (input) => {
4147
- const project = await requireProject(input);
4148
- if (!project.api.capabilities.reviewers.listCandidates) {
4149
- throw new PullRequestOperationFailed({
4150
- operation: "reviewerCandidates",
4151
- detail: "This host cannot say who may review a change request."
4152
- });
3809
+ }
3810
+ return claims;
3811
+ };
3812
+ var isArchivedWorkspaceProject = (project) => project.archivedAt != null;
3813
+ var listWorkspaceProjects = async (input) => {
3814
+ const projects = await input.listProjects();
3815
+ const claims = claimTable(input.apis);
3816
+ const byKind = new Map(input.apis.map((api) => [api.kind, api]));
3817
+ const supported = [];
3818
+ const unimplemented = /* @__PURE__ */ new Map();
3819
+ const viewerRoots = /* @__PURE__ */ new Map();
3820
+ const seen = /* @__PURE__ */ new Set();
3821
+ for (const project of projects) {
3822
+ if (input.filter.projectId !== void 0 && project.projectId !== input.filter.projectId) {
3823
+ continue;
4153
3824
  }
4154
- const viewer = await viewerPermissionsOf(project, input, "reviewerCandidates");
4155
- if (!viewer.requestReviewers) {
4156
- throw new PullRequestOperationFailed({
4157
- operation: "reviewerCandidates",
4158
- detail: REVIEWER_REQUEST_REFUSAL
4159
- });
3825
+ if (input.filter.projectId === void 0 && isArchivedWorkspaceProject(project)) continue;
3826
+ if (input.filter.projectIds !== void 0 && !input.filter.projectIds.includes(project.projectId)) {
3827
+ continue;
4160
3828
  }
4161
- return project.api.listReviewerCandidates(changeRequestRef(project, input)).catch((error) => {
3829
+ const identity = project.repositoryIdentity;
3830
+ if (identity === null) continue;
3831
+ const repository = repositoryIdentityOf(identity);
3832
+ if (repository === null) continue;
3833
+ let kind = identity.provider;
3834
+ if (kind === "unknown") {
3835
+ const claimed = claims.get(remoteHostOf(identity.remoteUrl));
3836
+ if (claimed !== void 0) kind = claimed;
3837
+ }
3838
+ const host = hostOf(identity, kind);
3839
+ if (input.filter.host !== void 0 && host !== input.filter.host.trim().toLowerCase()) {
3840
+ continue;
3841
+ }
3842
+ const api = byKind.get(kind) ?? null;
3843
+ if (api !== null) {
3844
+ const roots = viewerRoots.get(host);
3845
+ if (roots === void 0) viewerRoots.set(host, [project.workspaceRoot]);
3846
+ else if (!roots.includes(project.workspaceRoot)) roots.push(project.workspaceRoot);
3847
+ }
3848
+ const key = listCursorKey(host, repository);
3849
+ if (seen.has(key)) continue;
3850
+ seen.add(key);
3851
+ if (api === null) {
3852
+ const counted = unimplemented.get(host);
3853
+ if (counted === void 0) unimplemented.set(host, { kind, projectCount: 1 });
3854
+ else counted.projectCount += 1;
3855
+ continue;
3856
+ }
3857
+ supported.push({
3858
+ projectId: project.projectId,
3859
+ projectTitle: project.title,
3860
+ workspaceRoot: project.workspaceRoot,
3861
+ api: withRateLimitBackoff(api, host, input.backoff),
3862
+ repository,
3863
+ host
3864
+ });
3865
+ }
3866
+ return { supported, unimplemented, viewerRoots };
3867
+ };
3868
+
3869
+ // src/service/writes.ts
3870
+ var VERDICT_LABELS = {
3871
+ comment: "review",
3872
+ approve: "approve",
3873
+ "request-changes": "request changes on"
3874
+ };
3875
+ var ACTION_ACCESS_REFUSALS = {
3876
+ merge: "You need write access on this repository to merge.",
3877
+ ready: "You need write access on this repository, or to have opened this change request, to mark it ready for review.",
3878
+ draft: "You need write access on this repository, or to have opened this change request, to return it to a draft.",
3879
+ close: "You need write access on this repository, or to have opened this change request, to close it.",
3880
+ "update-branch": "You need write access on this repository, or to have opened this change request, to update its branch.",
3881
+ reopen: "You need write access on this repository, or to have opened this change request, to reopen it.",
3882
+ "enable-auto-merge": "You need write access on this repository to have it merged for you once it is ready.",
3883
+ "disable-auto-merge": "You need write access on this repository to stop it being merged for you once it is ready."
3884
+ };
3885
+ var REVIEWER_REQUEST_REFUSAL = "You need write access on this repository to ask for a review.";
3886
+ var makePullRequestWrites = (deps) => {
3887
+ const { requireProject, changeRequestRef, viewerPermissionsOf, afterMutation, options } = deps;
3888
+ const reviewerCandidates = async (input) => {
3889
+ const project = await requireProject(input);
3890
+ if (!project.api.capabilities.reviewers.listCandidates) {
3891
+ throw new PullRequestOperationFailed({
3892
+ operation: "reviewerCandidates",
3893
+ detail: "This host cannot say who may review a change request."
3894
+ });
3895
+ }
3896
+ const viewer = await viewerPermissionsOf(project, input, "reviewerCandidates");
3897
+ if (!viewer.requestReviewers) {
3898
+ throw new PullRequestOperationFailed({
3899
+ operation: "reviewerCandidates",
3900
+ detail: REVIEWER_REQUEST_REFUSAL
3901
+ });
3902
+ }
3903
+ return project.api.listReviewerCandidates(changeRequestRef(project, input)).catch((error) => {
4162
3904
  throw toPullRequestError("reviewerCandidates", error, project.api.kind, project.host);
4163
3905
  });
4164
3906
  };
@@ -4312,8 +4054,8 @@ ${cursorOf(project)?.updatedBefore ?? ""}`;
4312
4054
  const submitReview3 = async (input) => {
4313
4055
  const project = await requireProject(input);
4314
4056
  const review = project.api.capabilities.review;
4315
- const refuse = (detail2) => {
4316
- throw new PullRequestOperationFailed({ operation: "submitReview", detail: detail2 });
4057
+ const refuse = (detail) => {
4058
+ throw new PullRequestOperationFailed({ operation: "submitReview", detail });
4317
4059
  };
4318
4060
  if (!review.verdicts.includes(input.verdict)) {
4319
4061
  refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
@@ -4441,6 +4183,290 @@ ${cursorOf(project)?.updatedBefore ?? ""}`;
4441
4183
  return setReaction3(input);
4442
4184
  }
4443
4185
  };
4186
+ return { reviewerCandidates, act, converse };
4187
+ };
4188
+
4189
+ // src/service/pullRequestService.ts
4190
+ var LIST_CACHE_TTL_MS = 3e4;
4191
+ var DETAIL_CACHE_TTL_MS = 15e3;
4192
+ var DIFF_CACHE_TTL_MS = 6e4;
4193
+ var COMMIT_DIFF_CACHE_TTL_MS = 10 * 6e4;
4194
+ var LIST_STATS_CACHE_TTL_MS = 6e4;
4195
+ var DIFF_STALE_WINDOW_MS = 10 * 6e4;
4196
+ var LIST_CACHE_CAPACITY = 64;
4197
+ var LIST_STATS_CACHE_CAPACITY = 32;
4198
+ var DETAIL_CACHE_CAPACITY = 128;
4199
+ var DIFF_CACHE_CAPACITY = 128;
4200
+ var REF_EPOCH_CAPACITY = 2048;
4201
+ var createPullRequestService = (options) => {
4202
+ const now = options.now ?? Date.now;
4203
+ const backoff = createBackoff({ now });
4204
+ const workspace = (filter) => listWorkspaceProjects({
4205
+ listProjects: options.listProjects,
4206
+ apis: options.apis,
4207
+ backoff,
4208
+ filter
4209
+ }).catch(() => {
4210
+ throw new PullRequestOperationFailed({
4211
+ operation: "listProjects",
4212
+ detail: "The project list could not be read."
4213
+ });
4214
+ });
4215
+ const requireProject = async (ref) => {
4216
+ const { supported } = await workspace({ projectId: ref.projectId });
4217
+ const match = supported[0];
4218
+ if (!match) throw new PullRequestUnavailable({ reason: "provider-unsupported" });
4219
+ if (match.repository.toLowerCase() !== ref.repository.trim().toLowerCase()) {
4220
+ throw new PullRequestOperationFailed({
4221
+ operation: "resolveRepository",
4222
+ detail: "The change request does not belong to the selected project."
4223
+ });
4224
+ }
4225
+ return match;
4226
+ };
4227
+ const changeRequestRef = (project, ref) => ({
4228
+ cwd: project.workspaceRoot,
4229
+ repository: project.repository,
4230
+ host: project.host,
4231
+ number: ref.number
4232
+ });
4233
+ const viewerPermissionsOf = async (project, ref, operation) => {
4234
+ try {
4235
+ return await project.api.getViewerPermissions(changeRequestRef(project, ref));
4236
+ } catch (error) {
4237
+ throw toPullRequestError(operation, error, project.api.kind, project.host);
4238
+ }
4239
+ };
4240
+ const viewersByHost = /* @__PURE__ */ new Map();
4241
+ const { listUncached, listStatsUncached, viewerOf } = makePullRequestListReads({
4242
+ workspace,
4243
+ now,
4244
+ viewersByHost
4245
+ });
4246
+ const detailUncached = async (input) => {
4247
+ const project = await requireProject(input);
4248
+ const [changeRequest, viewer] = await Promise.all([
4249
+ project.api.getChangeRequest(changeRequestRef(project, input)).catch((error) => {
4250
+ throw toPullRequestError("detail", error, project.api.kind, project.host);
4251
+ }),
4252
+ viewerOf(project)
4253
+ ]);
4254
+ return {
4255
+ provider: project.api.kind,
4256
+ capabilities: project.api.capabilities,
4257
+ projectId: project.projectId,
4258
+ projectTitle: project.projectTitle,
4259
+ workspaceRoot: project.workspaceRoot,
4260
+ repository: project.repository,
4261
+ number: changeRequest.number,
4262
+ title: changeRequest.title,
4263
+ body: changeRequest.body,
4264
+ url: changeRequest.url,
4265
+ author: changeRequest.author,
4266
+ state: changeRequest.state,
4267
+ isDraft: changeRequest.isDraft,
4268
+ mergeability: changeRequest.mergeability,
4269
+ additions: changeRequest.additions,
4270
+ deletions: changeRequest.deletions,
4271
+ changedFiles: changeRequest.changedFiles,
4272
+ headBranch: changeRequest.headBranch,
4273
+ baseBranch: changeRequest.baseBranch,
4274
+ createdAt: changeRequest.createdAt,
4275
+ updatedAt: changeRequest.updatedAt,
4276
+ mergedAt: changeRequest.mergedAt,
4277
+ closedAt: changeRequest.closedAt,
4278
+ reviewers: changeRequest.reviewers,
4279
+ labels: changeRequest.labels,
4280
+ checks: changeRequest.checks,
4281
+ mergeCapabilities: changeRequest.mergeCapabilities,
4282
+ viewerPermissions: changeRequest.viewerPermissions,
4283
+ ...viewer === null || viewer.trim().length === 0 ? {} : { viewer },
4284
+ ...changeRequest.baseComparison === void 0 ? {} : { baseComparison: changeRequest.baseComparison },
4285
+ ...changeRequest.behindBy === void 0 ? {} : { behindBy: changeRequest.behindBy },
4286
+ ...changeRequest.autoMergeEnabled === void 0 ? {} : { autoMergeEnabled: changeRequest.autoMergeEnabled }
4287
+ };
4288
+ };
4289
+ const activityUncached = async (input) => {
4290
+ const project = await requireProject(input);
4291
+ const activity2 = await project.api.getChangeRequestActivity(changeRequestRef(project, input)).catch((error) => {
4292
+ throw toPullRequestError("activity", error, project.api.kind, project.host);
4293
+ });
4294
+ return {
4295
+ ...activity2.author === void 0 ? {} : { author: activity2.author },
4296
+ ...activity2.reviewers === void 0 ? {} : { reviewers: activity2.reviewers },
4297
+ comments: activity2.comments,
4298
+ commentCount: activity2.commentCount,
4299
+ commentsTruncated: activity2.commentsTruncated,
4300
+ reviewThreads: activity2.reviewThreads,
4301
+ commits: activity2.commits,
4302
+ ...activity2.reactions === void 0 ? {} : { reactions: activity2.reactions }
4303
+ };
4304
+ };
4305
+ const threadComments = async (input) => {
4306
+ const project = await requireProject(input);
4307
+ const read = project.api.getReviewThreadComments;
4308
+ if (read === void 0) {
4309
+ throw new PullRequestOperationFailed({
4310
+ operation: "threadComments",
4311
+ detail: "This host does not page review thread comments."
4312
+ });
4313
+ }
4314
+ return read({
4315
+ ...changeRequestRef(project, input),
4316
+ threadId: input.threadId,
4317
+ cursor: input.cursor
4318
+ }).catch((error) => {
4319
+ throw toPullRequestError("threadComments", error, project.api.kind, project.host);
4320
+ });
4321
+ };
4322
+ const diffUncached = async (input) => {
4323
+ const project = await requireProject(input);
4324
+ if (!project.api.capabilities.diff) {
4325
+ throw new PullRequestOperationFailed({
4326
+ operation: "diff",
4327
+ detail: "This host cannot provide a diff for a change request."
4328
+ });
4329
+ }
4330
+ return project.api.getDiff({
4331
+ ...changeRequestRef(project, input),
4332
+ ...input.cursor === void 0 ? {} : { cursor: input.cursor },
4333
+ ...input.commit === void 0 ? {} : { commit: input.commit }
4334
+ }).catch((error) => {
4335
+ throw toPullRequestError("diff", error, project.api.kind, project.host);
4336
+ });
4337
+ };
4338
+ const diffFileContents = async (input) => {
4339
+ const project = await requireProject(input);
4340
+ const read = project.api.getDiffFileContents;
4341
+ if (!project.api.capabilities.diff || read === void 0) {
4342
+ throw new PullRequestOperationFailed({
4343
+ operation: "diffFileContents",
4344
+ detail: "This host cannot expand unchanged pull request lines."
4345
+ });
4346
+ }
4347
+ return read({
4348
+ ...changeRequestRef(project, input),
4349
+ ...input.commit === void 0 ? {} : { commit: input.commit },
4350
+ changeType: input.changeType,
4351
+ oldPath: input.oldPath,
4352
+ newPath: input.newPath
4353
+ }).catch((error) => {
4354
+ throw toPullRequestError("diffFileContents", error, project.api.kind, project.host);
4355
+ });
4356
+ };
4357
+ let epochCounter = 0;
4358
+ let listingsEpoch = 0;
4359
+ const refEpochs = /* @__PURE__ */ new Map();
4360
+ const refScope = (ref) => `${ref.projectId} ${ref.repository} ${ref.number}`;
4361
+ const refEpoch = (ref) => refEpochs.get(refScope(ref)) ?? 0;
4362
+ const bumpRefEpoch = (ref) => {
4363
+ const scope = refScope(ref);
4364
+ if (!refEpochs.has(scope) && refEpochs.size >= REF_EPOCH_CAPACITY) {
4365
+ const oldest = refEpochs.keys().next().value;
4366
+ if (oldest !== void 0) refEpochs.delete(oldest);
4367
+ }
4368
+ refEpochs.set(scope, ++epochCounter);
4369
+ };
4370
+ const listCache = createCache({
4371
+ capacity: LIST_CACHE_CAPACITY,
4372
+ ttlMs: () => LIST_CACHE_TTL_MS,
4373
+ now
4374
+ });
4375
+ const detailCache = createCache({
4376
+ capacity: DETAIL_CACHE_CAPACITY,
4377
+ ttlMs: () => DETAIL_CACHE_TTL_MS,
4378
+ now
4379
+ });
4380
+ const activityCache = createCache({
4381
+ capacity: DETAIL_CACHE_CAPACITY,
4382
+ ttlMs: () => DETAIL_CACHE_TTL_MS,
4383
+ now
4384
+ });
4385
+ const diffCache = createCache({
4386
+ capacity: DIFF_CACHE_CAPACITY,
4387
+ // commit 的 diff 内容寻址,活得比整份 diff 久。
4388
+ ttlMs: (key) => JSON.parse(key)[5] === null ? DIFF_CACHE_TTL_MS : COMMIT_DIFF_CACHE_TTL_MS,
4389
+ now
4390
+ });
4391
+ const listStatsCache = createCache({
4392
+ capacity: LIST_STATS_CACHE_CAPACITY,
4393
+ ttlMs: () => LIST_STATS_CACHE_TTL_MS,
4394
+ now
4395
+ });
4396
+ const staleDiff = createStaleWindow({
4397
+ capacity: DIFF_CACHE_CAPACITY,
4398
+ staleMs: DIFF_STALE_WINDOW_MS,
4399
+ now,
4400
+ ...options.onRefreshFailure === void 0 ? {} : { onRefreshFailure: options.onRefreshFailure }
4401
+ });
4402
+ const refKey = (ref) => JSON.stringify([refEpoch(ref), ref.projectId, ref.repository, ref.number]);
4403
+ const list = (input) => {
4404
+ const key = JSON.stringify([
4405
+ listingsEpoch,
4406
+ input.state,
4407
+ input.involvement ?? null,
4408
+ // 按位置写,所以同一组筛选无论记录怎么拼出来都是同一个键。
4409
+ input.filters === void 0 ? null : [
4410
+ input.filters.draft ?? null,
4411
+ input.filters.review ?? null,
4412
+ input.filters.checks ?? null,
4413
+ input.filters.author ?? null,
4414
+ input.filters.labels ?? null,
4415
+ input.filters.excludedLabels ?? null
4416
+ ],
4417
+ input.projectId ?? null,
4418
+ input.projectIds === void 0 ? null : [...input.projectIds].sort(),
4419
+ input.host ?? null,
4420
+ input.limit ?? null,
4421
+ input.query ?? null,
4422
+ input.cursors === void 0 ? null : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right))
4423
+ ]);
4424
+ return listCache.get(key, () => listUncached(input));
4425
+ };
4426
+ const listStats = (input) => {
4427
+ if (input.refs.length === 0) return Promise.resolve({ stats: [] });
4428
+ const key = JSON.stringify([
4429
+ listingsEpoch,
4430
+ input.refs.map((ref) => [ref.projectId, ref.repository, ref.number]).toSorted(
4431
+ (left, right) => `${left[0]} ${left[1]} ${left[2]}`.localeCompare(`${right[0]} ${right[1]} ${right[2]}`)
4432
+ )
4433
+ ]);
4434
+ return listStatsCache.get(key, () => listStatsUncached(input));
4435
+ };
4436
+ const detail = (input) => detailCache.get(refKey(input), () => detailUncached(input));
4437
+ const activity = (input) => activityCache.get(refKey(input), () => activityUncached(input));
4438
+ const diff = (input) => {
4439
+ const key = JSON.stringify([
4440
+ refEpoch(input),
4441
+ input.projectId,
4442
+ input.repository,
4443
+ input.number,
4444
+ input.cursor ?? null,
4445
+ input.commit ?? null
4446
+ ]);
4447
+ return staleDiff.get(key, () => diffCache.get(key, () => diffUncached(input)));
4448
+ };
4449
+ const invalidate = (input) => {
4450
+ if (input.reference === void 0) {
4451
+ listingsEpoch = ++epochCounter;
4452
+ viewersByHost.clear();
4453
+ return Promise.resolve(null);
4454
+ }
4455
+ bumpRefEpoch(input.reference);
4456
+ return Promise.resolve(null);
4457
+ };
4458
+ const afterMutation = (ref) => {
4459
+ bumpRefEpoch(ref);
4460
+ listingsEpoch = ++epochCounter;
4461
+ return null;
4462
+ };
4463
+ const { reviewerCandidates, act, converse } = makePullRequestWrites({
4464
+ requireProject,
4465
+ changeRequestRef,
4466
+ viewerPermissionsOf,
4467
+ afterMutation,
4468
+ options
4469
+ });
4444
4470
  return {
4445
4471
  list,
4446
4472
  listStats,