@gitkraken/core-gitlens 0.7.0 → 0.7.2
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/CHANGELOG.md +28 -2
- package/dist/git/remotes/azure-devops.js +3 -3
- package/dist/git/remotes/azure-devops.js.map +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
- package/dist/plus/integrations/collectionMetadata.js +138 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +3 -1
- package/dist/plus/integrations/errors.js +7 -3
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +35 -1
- package/dist/plus/integrations/integrationService.js +90 -43
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/lite.js +5 -4
- package/dist/plus/integrations/lite.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +68 -23
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
- package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +70 -12
- package/dist/plus/integrations/models/integration.js +56 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +17 -0
- package/dist/plus/integrations/models/issuesIntegration.js +2 -0
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
- package/dist/plus/integrations/providers/azure/azure.js +192 -12
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
- package/dist/plus/integrations/providers/azure/search.js +250 -0
- package/dist/plus/integrations/providers/azure/search.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
- package/dist/plus/integrations/providers/azureDevOps.js +191 -122
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
- package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
- package/dist/plus/integrations/providers/bitbucket.js +13 -2
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +6 -6
- package/dist/plus/integrations/providers/github.js +35 -34
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
- package/dist/plus/integrations/providers/gitlab.js +32 -28
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
- package/dist/plus/integrations/providers/jira-server.js +15 -7
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +9 -0
- package/dist/plus/integrations/providers/jira.js +14 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +2 -2
- package/dist/plus/integrations/providers/models.d.ts +10 -1
- package/dist/plus/integrations/providers/models.js +86 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
- package/dist/plus/integrations/providers/providersApi.js +18 -3
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +12 -1
- package/dist/plus/integrations/providers/utils.js +36 -6
- package/dist/plus/integrations/providers/utils.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +6 -6
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/context.d.ts +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +18 -4
- package/dist/plus/integrations/reads/counts.js +66 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +2 -1
- package/dist/plus/integrations/reads/drains.js +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +1 -1
- package/dist/plus/integrations/reads/filters.js +17 -12
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +2 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.js +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +2 -2
- package/dist/plus/integrations/reads/issues.js +2 -2
- package/dist/plus/integrations/reads/paging.js +1 -1
- package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
- package/dist/plus/integrations/reads/resolveRepository.js +110 -19
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +4 -2
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
- package/dist/plus/integrations/reads/trackerIssue.js +15 -9
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +6 -2
- package/dist/plus/integrations/reads/warnings.js +15 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +84 -6
- package/dist/plus/integrations/results.js +9 -2
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
- package/dist/plus/integrations/utils/domain.utils.js +36 -1
- package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
- package/dist/plus/integrations/utils/integration.utils.js +12 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/docs/integrations.md +263 -74
- package/docs/kepler-read-api-parity.md +19 -6
- package/package.json +1 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { CancellationError, isCancellationError } from "../../../../utils/cancellation.js";
|
|
2
|
+
import { Logger as _logger } from "../../../../utils/logger.js";
|
|
3
|
+
import { mapSettledBounded } from "../../../../utils/promise.js";
|
|
4
|
+
import { AuthenticationError } from "../../../../git/errors.js";
|
|
5
|
+
import { getNonce, sha256 } from "../../../../utils/crypto.js";
|
|
6
|
+
import "../../../../git/models/pullRequest.js";
|
|
7
|
+
import { getPullRequestComparator } from "../../../../git/utils/pullRequest.utils.js";
|
|
8
|
+
import { throwProviderError } from "../providerErrors.js";
|
|
9
|
+
import { toCollectionScopeFailure } from "../../collectionMetadata.js";
|
|
10
|
+
import { fromProviderPullRequest } from "../models.js";
|
|
11
|
+
import { normalizeBitbucketServerPullRequest } from "./models.js";
|
|
12
|
+
//#region ../plus/integrations/src/providers/bitbucket-server/pullRequestSearch.ts
|
|
13
|
+
/**
|
|
14
|
+
* The relationships Bitbucket Data Center can express, in the canonical order its facets (and so its cursor) are
|
|
15
|
+
* built in. A Bitbucket pull request has no assignee, and none of its pull-request endpoints filter by mention.
|
|
16
|
+
*/
|
|
17
|
+
const relationships = [
|
|
18
|
+
"author",
|
|
19
|
+
"review-requested",
|
|
20
|
+
"reviewed-by"
|
|
21
|
+
];
|
|
22
|
+
/** Bitbucket answers an unspecified page size with 25; a search page asks for this many per facet instead. */
|
|
23
|
+
const defaultSearchPageSize = 30;
|
|
24
|
+
/**
|
|
25
|
+
* The server's own cap on a page (`page.max.pullrequests`, 1000 unless an administrator lowered it), and the one
|
|
26
|
+
* page a count reads per facet: past it the count is a floor rather than more of a customer-run server's capacity
|
|
27
|
+
* spent on a figure the caller uses as a cost hint.
|
|
28
|
+
*/
|
|
29
|
+
const maxPageSize = 1e3;
|
|
30
|
+
/** Matches the SDK's own cap on concurrent Bitbucket Data Center pull-request requests. */
|
|
31
|
+
const facetConcurrency = 5;
|
|
32
|
+
/** The server states each facade state covers. `closed` is two, so it can't be sent as one `state` filter. */
|
|
33
|
+
const pullRequestStates = {
|
|
34
|
+
open: ["OPEN"],
|
|
35
|
+
closed: ["DECLINED", "SUPERSEDED"],
|
|
36
|
+
merged: ["MERGED"]
|
|
37
|
+
};
|
|
38
|
+
const orderParams = {
|
|
39
|
+
"updated:desc": "NEWEST",
|
|
40
|
+
"updated:asc": "OLDEST"
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Pull requests matching `criteria` over the requested repositories, or over the current user's relationships when
|
|
44
|
+
* none are given.
|
|
45
|
+
*
|
|
46
|
+
* Every facet reads its own page, and the merged page is re-ordered as a whole. Each facet keeps its own
|
|
47
|
+
* `nextPageStart` in the cursor, which is bound to this connection, installation and query: a cursor handed back
|
|
48
|
+
* under anything else is refused rather than read as page 1 of a different search. A pull request several
|
|
49
|
+
* relationships match is served by exactly one of them, the first in canonical order (see {@link ownsRow}), so facets
|
|
50
|
+
* that advance independently never serve it twice across pages and the cursor carries no history of what it emitted.
|
|
51
|
+
*
|
|
52
|
+
* Every criterion the server applies is applied again to the rows it returns, so a server that ignores a parameter
|
|
53
|
+
* (one older than 8.18, which shipped drafts, has no `draft`) narrows the page instead of silently widening it.
|
|
54
|
+
*
|
|
55
|
+
* A facet that fails is reported in `metadata` while its siblings still answer, and keeps its offset, so the next page
|
|
56
|
+
* retries the page it missed — a transient failure recovers the rows it cost. A facet that fails its retry too is
|
|
57
|
+
* dropped, and reported on every later page, which no longer reads it; one retry keeps a facet that will never answer
|
|
58
|
+
* from holding `hasMore` open forever. If every facet fails on the first page, the first failure is thrown instead.
|
|
59
|
+
*/
|
|
60
|
+
async function searchBitbucketServerPullRequestsPage(request, token, options, cancellation) {
|
|
61
|
+
const query = toQuery(options);
|
|
62
|
+
const comparator = getPullRequestComparator(query.sort);
|
|
63
|
+
const key = await getCursorKey(options.connectionId, query);
|
|
64
|
+
const cursor = await parseCursor(options.cursor, key, query.facets);
|
|
65
|
+
const active = query.facets.flatMap((facet, index) => cursor.starts[index] != null ? [{
|
|
66
|
+
facet,
|
|
67
|
+
start: cursor.starts[index],
|
|
68
|
+
index
|
|
69
|
+
}] : []);
|
|
70
|
+
const failed = [...cursor.failed];
|
|
71
|
+
const failures = failed.map((f) => toEarlierFailure(token, query.facets[f.facet], f.kind));
|
|
72
|
+
const retrying = [];
|
|
73
|
+
if (active.length === 0) return {
|
|
74
|
+
values: [],
|
|
75
|
+
hasMore: false,
|
|
76
|
+
page: cursor.page,
|
|
77
|
+
truncated: cursor.truncated,
|
|
78
|
+
metadata: toMetadata(failures)
|
|
79
|
+
};
|
|
80
|
+
const limit = Math.min(maxPageSize, Math.max(1, Math.trunc(options.pageSize ?? defaultSearchPageSize)));
|
|
81
|
+
const results = await mapSettledBounded(active, facetConcurrency, ({ facet, start }) => readFacetPage(request, token, query, facet, start, limit, false, cancellation));
|
|
82
|
+
const rejected = results.filter((r) => r.status === "rejected");
|
|
83
|
+
const cancelled = rejected.find((r) => isCancellationError(r.reason));
|
|
84
|
+
if (cancelled != null) throw cancelled.reason;
|
|
85
|
+
const credential = rejected.find((r) => r.reason instanceof AuthenticationError);
|
|
86
|
+
if (credential != null) throw credential.reason;
|
|
87
|
+
if (cursor.page === 1 && rejected.length === results.length) throw rejected[0].reason;
|
|
88
|
+
const nextStarts = cursor.starts.map(() => null);
|
|
89
|
+
const values = [];
|
|
90
|
+
let truncated = cursor.truncated;
|
|
91
|
+
for (let i = 0; i < results.length; i++) {
|
|
92
|
+
const { facet, index } = active[i];
|
|
93
|
+
const result = results[i];
|
|
94
|
+
if (result.status === "rejected") {
|
|
95
|
+
const failure = toCollectionScopeFailure(toCollectionScope(token, facet), result.reason);
|
|
96
|
+
failures.push(failure);
|
|
97
|
+
if (cursor.retrying.includes(index)) failed.push({
|
|
98
|
+
facet: index,
|
|
99
|
+
kind: failure.kind
|
|
100
|
+
});
|
|
101
|
+
else {
|
|
102
|
+
nextStarts[index] = active[i].start;
|
|
103
|
+
retrying.push(index);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
nextStarts[index] = result.value.next;
|
|
108
|
+
truncated ||= result.value.stalled;
|
|
109
|
+
for (const { raw, normalized } of result.value.matches) {
|
|
110
|
+
if (!ownsRow(raw, facet, query)) continue;
|
|
111
|
+
values.push(normalized);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const hasMore = nextStarts.some((start) => start != null);
|
|
115
|
+
const state = {
|
|
116
|
+
failed,
|
|
117
|
+
retrying,
|
|
118
|
+
truncated
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
values: values.map((pr) => fromProviderPullRequest(pr, options.provider, { currentAccountId: options.currentUser?.id })).sort(comparator),
|
|
122
|
+
cursor: hasMore ? JSON.stringify({
|
|
123
|
+
key,
|
|
124
|
+
page: cursor.page + 1,
|
|
125
|
+
starts: nextStarts,
|
|
126
|
+
state: {
|
|
127
|
+
...state,
|
|
128
|
+
signature: await signState(key, cursor.page + 1, nextStarts, state)
|
|
129
|
+
}
|
|
130
|
+
}) : void 0,
|
|
131
|
+
hasMore,
|
|
132
|
+
page: cursor.page,
|
|
133
|
+
truncated,
|
|
134
|
+
metadata: toMetadata(failures)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function toMetadata(failures) {
|
|
138
|
+
return failures.length > 0 ? {
|
|
139
|
+
completeness: "partial",
|
|
140
|
+
failures
|
|
141
|
+
} : void 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A facet failure carried over from an earlier page, rebuilt from the query rather than from the cursor: the cursor
|
|
145
|
+
* records only which facet failed and how, so it can't name a scope or a message the read never produced.
|
|
146
|
+
*/
|
|
147
|
+
function toEarlierFailure(token, facet, kind) {
|
|
148
|
+
return {
|
|
149
|
+
scope: toCollectionScope(token, facet),
|
|
150
|
+
kind,
|
|
151
|
+
message: "Failed on an earlier page of this search, so later pages did not read it"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* How many pull requests match `criteria`: the same facets, predicates and identity the search uses, so the count
|
|
156
|
+
* is the number of distinct rows the search would return — the union of the requested states, not the largest of
|
|
157
|
+
* them, since Bitbucket has no per-search ceiling for a maximum to stay under.
|
|
158
|
+
*
|
|
159
|
+
* Bitbucket Data Center has no count query and reports no total, so each facet reads one page of up to
|
|
160
|
+
* {@link maxPageSize} and the count is a floor (`lowerBound`) when any facet had more. Unlike the search, one failed
|
|
161
|
+
* facet fails the whole count: a count missing a facet is a wrong number rather than a partial one.
|
|
162
|
+
*/
|
|
163
|
+
async function countBitbucketServerPullRequests(request, token, options, cancellation) {
|
|
164
|
+
const query = toQuery(options);
|
|
165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
166
|
+
let lowerBound = false;
|
|
167
|
+
for (const facet of query.facets) {
|
|
168
|
+
const result = await readFacetPage(request, token, query, facet, 0, maxPageSize, true, cancellation);
|
|
169
|
+
for (const { raw } of result.matches) seen.add(toIdentity(raw));
|
|
170
|
+
lowerBound ||= result.next != null || result.stalled;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
count: seen.size,
|
|
174
|
+
lowerBound: lowerBound || void 0
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the criteria into the facets and predicates every page applies, refusing — by throwing, which the read
|
|
179
|
+
* reports as a failed request — anything the capability table does not declare. The facade validates the same
|
|
180
|
+
* table first, so reaching a refusal here means a caller bypassed it; refusing rather than dropping the criterion
|
|
181
|
+
* keeps that caller from reading a wider set than it asked for.
|
|
182
|
+
*/
|
|
183
|
+
function toQuery(options) {
|
|
184
|
+
if (options.org != null && options.org.length > 0) throw new Error("Bitbucket Data Center cannot scope a pull request search to a project");
|
|
185
|
+
const criteria = options.criteria;
|
|
186
|
+
if (criteria?.updatedAfter != null || criteria?.createdAfter != null) throw new Error("Bitbucket Data Center cannot filter a pull request search by date");
|
|
187
|
+
const sort = criteria?.sort ?? "updated:desc";
|
|
188
|
+
if (orderParams[sort] == null) throw new Error(`Bitbucket Data Center cannot order a pull request search by '${sort}'`);
|
|
189
|
+
const requested = criteria?.relationships ?? [];
|
|
190
|
+
const unsupported = requested.find((r) => !relationships.includes(r));
|
|
191
|
+
if (unsupported != null) throw new Error(`Bitbucket Data Center cannot search pull requests by the '${unsupported}' relationship`);
|
|
192
|
+
const facetRelationships = relationships.filter((r) => requested.includes(r));
|
|
193
|
+
if (facetRelationships.length > 0 && options.currentUser == null) throw new Error("A Bitbucket Data Center relationship search requires the current user");
|
|
194
|
+
const requestedStates = criteria?.states?.length ? criteria.states : ["open"];
|
|
195
|
+
const states = requestedStates.includes("all") ? void 0 : new Set(requestedStates.flatMap((s) => pullRequestStates[s]));
|
|
196
|
+
const repos = [...new Map((options.repos ?? []).map((r) => [repoKey(r), r])).entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, repo]) => repo);
|
|
197
|
+
for (const repo of repos) for (const segment of [repo.namespace, repo.name]) if (!segment || segment === "." || segment === "..") throw new Error("Invalid Bitbucket Data Center repository for a pull request search");
|
|
198
|
+
let facets;
|
|
199
|
+
if (repos.length > 0) {
|
|
200
|
+
const scoped = facetRelationships.length > 0 ? facetRelationships : [void 0];
|
|
201
|
+
facets = repos.flatMap((repo) => scoped.map((relationship) => ({
|
|
202
|
+
repo,
|
|
203
|
+
relationship
|
|
204
|
+
})));
|
|
205
|
+
} else if (facetRelationships.length > 0) facets = facetRelationships.flatMap((relationship) => relationship === "reviewed-by" ? [{
|
|
206
|
+
relationship,
|
|
207
|
+
role: "REVIEWER"
|
|
208
|
+
}, {
|
|
209
|
+
relationship,
|
|
210
|
+
role: "PARTICIPANT"
|
|
211
|
+
}] : [{ relationship }]);
|
|
212
|
+
else throw new Error("A Bitbucket Data Center pull request search must be scoped by repositories or a relationship");
|
|
213
|
+
return {
|
|
214
|
+
...options,
|
|
215
|
+
facets,
|
|
216
|
+
relationships: facetRelationships,
|
|
217
|
+
states,
|
|
218
|
+
sort
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Project keys and repository slugs resolve case-insensitively, so two spellings name one repository. */
|
|
222
|
+
function repoKey(repo) {
|
|
223
|
+
return `${repo.namespace}/${repo.name}`.toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A facet's classified failure, with the one correction the provider-wide classification can't make: a 403 on a
|
|
227
|
+
* REPOSITORY is Bitbucket refusing that repository (the token lacks `REPO_READ` there), not the credential failing.
|
|
228
|
+
*
|
|
229
|
+
* Made here, where every read of a facet passes, so the search and the count can't disagree about it on any path —
|
|
230
|
+
* a sibling repository's failure, a first page where every repository failed, or a count. Left an
|
|
231
|
+
* `AuthenticationError`, it would reach the integration's exception handling and mark a connection that works
|
|
232
|
+
* everywhere else for recovery and toward disconnecting; as a plain error naming the repository it is reported as
|
|
233
|
+
* that scope's failure alone. A 401 anywhere, and a 403 on the user's own dashboard, stay the credential failing.
|
|
234
|
+
*/
|
|
235
|
+
function toFacetError(ex, facet) {
|
|
236
|
+
if (!(ex instanceof AuthenticationError) || facet.repo == null) return ex;
|
|
237
|
+
if (ex.reason !== 3) return ex;
|
|
238
|
+
return new Error(`Bitbucket Data Center refused access to ${facet.repo.namespace}/${facet.repo.name}: ${(ex.original ?? ex).message}`, { cause: ex });
|
|
239
|
+
}
|
|
240
|
+
/** A pull request's identity across every facet and endpoint: its id is only unique within its repository. */
|
|
241
|
+
function toIdentity(pr) {
|
|
242
|
+
return `${pr.toRef.repository.id}/${pr.id}`;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Binds a cursor to everything that decides what it continues: the connection and installation it was read from,
|
|
246
|
+
* the facets and every criterion. Hashed, so neither the query nor the address is published inside the cursor.
|
|
247
|
+
*/
|
|
248
|
+
function getCursorKey(connectionId, query) {
|
|
249
|
+
return sha256(JSON.stringify([
|
|
250
|
+
connectionId,
|
|
251
|
+
query.baseUrl,
|
|
252
|
+
query.facets.map((f) => [
|
|
253
|
+
f.repo != null ? repoKey(f.repo) : null,
|
|
254
|
+
f.relationship ?? null,
|
|
255
|
+
f.role ?? null
|
|
256
|
+
]),
|
|
257
|
+
query.states != null ? [...query.states].sort() : null,
|
|
258
|
+
query.criteria?.text?.trim() ?? "",
|
|
259
|
+
query.criteria?.draft ?? null,
|
|
260
|
+
query.criteria?.includeArchived === true,
|
|
261
|
+
query.currentUser != null ? [query.currentUser.id, query.currentUser.username] : null,
|
|
262
|
+
query.sort
|
|
263
|
+
]));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Signs a continuation — its page, every facet's offset, which facets failed, and whether a facet stalled — so an
|
|
267
|
+
* edited cursor can't pass for complete: ending a facet early by nulling its offset, or claiming a failure its read
|
|
268
|
+
* never had. The key only binds a cursor to its query and travels inside it; this is keyed by a secret that never
|
|
269
|
+
* leaves this process, so a signature can't be recomputed outside it. A cursor that outlives the process keeps
|
|
270
|
+
* continuing, and reports its read as unconfirmed rather than as complete (see {@link parseCursor}).
|
|
271
|
+
*/
|
|
272
|
+
const stateSecret = getNonce();
|
|
273
|
+
function signState(key, page, starts, state) {
|
|
274
|
+
return sha256(JSON.stringify([
|
|
275
|
+
stateSecret,
|
|
276
|
+
key,
|
|
277
|
+
page,
|
|
278
|
+
starts,
|
|
279
|
+
state.failed,
|
|
280
|
+
state.retrying,
|
|
281
|
+
state.truncated
|
|
282
|
+
]));
|
|
283
|
+
}
|
|
284
|
+
/** The failure kinds a cursor may carry — the SDK's closed set, so a cursor can't name one the facade never maps. */
|
|
285
|
+
const failureKinds = /* @__PURE__ */ new Set([
|
|
286
|
+
"authentication",
|
|
287
|
+
"rate-limit",
|
|
288
|
+
"not-found",
|
|
289
|
+
"network",
|
|
290
|
+
"provider",
|
|
291
|
+
"unknown"
|
|
292
|
+
]);
|
|
293
|
+
/**
|
|
294
|
+
* Parses a cursor this module wrote for the same connection and query.
|
|
295
|
+
*
|
|
296
|
+
* The key binds a cursor to its connection, installation and query, but travels inside it, so the offsets can be
|
|
297
|
+
* edited; the signature below covers them, so an edited cursor still reads but can't report its read as complete. The completeness state is
|
|
298
|
+
* the part that could be turned against it, so it is signed and written on every continuation. A state that is
|
|
299
|
+
* missing or doesn't verify is not refused, since that is also what a cursor from an earlier process looks like, and
|
|
300
|
+
* failing a read that still works helps no one; but it can't be passed off as complete either — it degrades to
|
|
301
|
+
* `truncated`, "completeness not confirmed", without the failures it can't prove. A failure is rebuilt from the facet
|
|
302
|
+
* it names regardless, so even a verified one can't name a scope or text the read never produced.
|
|
303
|
+
*/
|
|
304
|
+
async function parseCursor(cursor, key, facets) {
|
|
305
|
+
const facetCount = facets.length;
|
|
306
|
+
if (cursor == null || cursor === "{}") return {
|
|
307
|
+
page: 1,
|
|
308
|
+
starts: new Array(facetCount).fill(0),
|
|
309
|
+
truncated: false,
|
|
310
|
+
failed: [],
|
|
311
|
+
retrying: []
|
|
312
|
+
};
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(cursor) ?? void 0;
|
|
316
|
+
} catch {}
|
|
317
|
+
const starts = parsed?.starts;
|
|
318
|
+
if (parsed == null || parsed.key !== key || typeof parsed.page !== "number" || !Number.isSafeInteger(parsed.page) || parsed.page < 2 || !Array.isArray(starts) || starts.length !== facetCount || !starts.every((start) => start === null || Number.isSafeInteger(start) && start >= 0) || !starts.some((start) => start !== null)) throw new Error("Invalid Bitbucket Data Center pull request search cursor for this connection or query");
|
|
319
|
+
return {
|
|
320
|
+
page: parsed.page,
|
|
321
|
+
starts,
|
|
322
|
+
...await parseState(parsed.state, key, parsed.page, starts)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The signed completeness state of a continuation, verified together with its page and offsets. Every continuation
|
|
327
|
+
* carries one, so a state that is missing, malformed, not signed by this process, or signed for other offsets says
|
|
328
|
+
* the read's completeness can't be confirmed — never that it was whole.
|
|
329
|
+
*/
|
|
330
|
+
async function parseState(state, key, page, starts) {
|
|
331
|
+
const unconfirmed = {
|
|
332
|
+
failed: [],
|
|
333
|
+
retrying: [],
|
|
334
|
+
truncated: true
|
|
335
|
+
};
|
|
336
|
+
if (state == null || typeof state !== "object") return unconfirmed;
|
|
337
|
+
const { failed, retrying, truncated, signature } = state;
|
|
338
|
+
if (typeof truncated !== "boolean" || !Array.isArray(retrying) || !retrying.every((i) => Number.isSafeInteger(i) && i >= 0 && i < starts.length && starts[i] !== null) || !Array.isArray(failed) || !failed.every((f) => f != null && Number.isSafeInteger(f.facet) && f.facet >= 0 && f.facet < starts.length && starts[f.facet] === null && typeof f.kind === "string" && failureKinds.has(f.kind))) return unconfirmed;
|
|
339
|
+
const verified = {
|
|
340
|
+
failed,
|
|
341
|
+
retrying,
|
|
342
|
+
truncated
|
|
343
|
+
};
|
|
344
|
+
return signature === await signState(key, page, starts, verified) ? verified : unconfirmed;
|
|
345
|
+
}
|
|
346
|
+
function toCollectionScope(token, facet) {
|
|
347
|
+
return facet.repo != null ? {
|
|
348
|
+
providerId: token.providerId,
|
|
349
|
+
repositoryId: `${facet.repo.namespace}/${facet.repo.name}`
|
|
350
|
+
} : { providerId: token.providerId };
|
|
351
|
+
}
|
|
352
|
+
async function readFacetPage(request, token, query, facet, start, limit, countOnly, cancellation) {
|
|
353
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
354
|
+
let body;
|
|
355
|
+
try {
|
|
356
|
+
({body} = await request({
|
|
357
|
+
url: toFacetUrl(query, facet, start, limit, countOnly),
|
|
358
|
+
headers: { Authorization: `Bearer ${token.accessToken}` },
|
|
359
|
+
signal: cancellation
|
|
360
|
+
}));
|
|
361
|
+
if (body == null || !Array.isArray(body.values) || typeof body.isLastPage !== "boolean" || body.start !== start) throw new Error("Invalid Bitbucket Data Center pull request page");
|
|
362
|
+
} catch (ex) {
|
|
363
|
+
if (isCancellationError(ex)) throw ex;
|
|
364
|
+
if (cancellation?.aborted) throw new CancellationError(ex instanceof Error ? ex : void 0);
|
|
365
|
+
try {
|
|
366
|
+
throwProviderError(token, ex);
|
|
367
|
+
} catch (classified) {
|
|
368
|
+
throw toFacetError(classified, facet);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (cancellation?.aborted) throw new CancellationError();
|
|
372
|
+
const next = body.nextPageStart;
|
|
373
|
+
const more = !body.isLastPage && Number.isSafeInteger(next) && next > start;
|
|
374
|
+
const matches = [];
|
|
375
|
+
for (const raw of body.values) {
|
|
376
|
+
if (!matchesFacet(raw, facet, query)) continue;
|
|
377
|
+
try {
|
|
378
|
+
matches.push({
|
|
379
|
+
raw,
|
|
380
|
+
normalized: normalizeBitbucketServerPullRequest(raw)
|
|
381
|
+
});
|
|
382
|
+
} catch (ex) {
|
|
383
|
+
_logger.warn(`Skipped an unmappable Bitbucket Data Center pull request; id=${raw.id}, ex=${String(ex)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
matches,
|
|
388
|
+
next: more ? next : null,
|
|
389
|
+
stalled: !body.isLastPage && !more
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function toFacetUrl(query, facet, start, limit, countOnly) {
|
|
393
|
+
const criteria = query.criteria;
|
|
394
|
+
const state = query.states?.size === 1 ? [...query.states][0] : void 0;
|
|
395
|
+
let url;
|
|
396
|
+
if (facet.repo != null) {
|
|
397
|
+
url = new URL(`${query.baseUrl}/projects/${encodeURIComponent(facet.repo.namespace)}/repos/${encodeURIComponent(facet.repo.name)}/pull-requests`);
|
|
398
|
+
url.searchParams.set("state", state ?? "ALL");
|
|
399
|
+
const text = criteria?.text?.trim();
|
|
400
|
+
if (text) url.searchParams.set("filterText", text);
|
|
401
|
+
if (criteria?.draft != null) url.searchParams.set("draft", String(criteria.draft));
|
|
402
|
+
if (facet.relationship != null) {
|
|
403
|
+
url.searchParams.set("username.1", query.currentUser.username);
|
|
404
|
+
switch (facet.relationship) {
|
|
405
|
+
case "author":
|
|
406
|
+
url.searchParams.set("role.1", "AUTHOR");
|
|
407
|
+
break;
|
|
408
|
+
case "review-requested":
|
|
409
|
+
url.searchParams.set("role.1", "REVIEWER");
|
|
410
|
+
url.searchParams.set("approved.1", "false");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (countOnly) url.searchParams.set("withProperties", "false");
|
|
414
|
+
} else {
|
|
415
|
+
url = new URL(`${query.baseUrl}/dashboard/pull-requests`);
|
|
416
|
+
if (state != null) url.searchParams.set("state", state);
|
|
417
|
+
switch (facet.relationship) {
|
|
418
|
+
case "author":
|
|
419
|
+
url.searchParams.set("role", "AUTHOR");
|
|
420
|
+
break;
|
|
421
|
+
case "review-requested":
|
|
422
|
+
url.searchParams.set("role", "REVIEWER");
|
|
423
|
+
url.searchParams.set("participantStatus", "UNAPPROVED");
|
|
424
|
+
break;
|
|
425
|
+
case "reviewed-by":
|
|
426
|
+
url.searchParams.set("role", facet.role);
|
|
427
|
+
url.searchParams.set("participantStatus", "APPROVED,NEEDS_WORK");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
url.searchParams.set("order", orderParams[query.sort]);
|
|
431
|
+
url.searchParams.set("start", String(start));
|
|
432
|
+
url.searchParams.set("limit", String(limit));
|
|
433
|
+
return url.toString();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Re-applies every constraint of the query to a row the server returned, so what the server was asked to filter and
|
|
437
|
+
* what it actually filtered can't differ in the result: the dashboard has no text or draft filter, a server older
|
|
438
|
+
* than 8.18 ignores `draft`, several states are read as every state, a reviewer filter can't tell a pending review
|
|
439
|
+
* from one that asked for changes, and no endpoint excludes archived repositories.
|
|
440
|
+
*/
|
|
441
|
+
function matchesFacet(pr, facet, query) {
|
|
442
|
+
const repository = pr.toRef?.repository;
|
|
443
|
+
if (pr.id == null || repository?.id == null) return false;
|
|
444
|
+
if (query.states != null && !query.states.has(pr.state)) return false;
|
|
445
|
+
if (facet.repo != null && `${repository.project?.key}/${repository.slug}`.toLowerCase() !== repoKey(facet.repo)) return false;
|
|
446
|
+
const criteria = query.criteria;
|
|
447
|
+
if (criteria?.includeArchived !== true && repository.archived === true) return false;
|
|
448
|
+
if (criteria?.draft != null && pr.draft === true !== criteria.draft) return false;
|
|
449
|
+
const text = criteria?.text?.trim().toLowerCase();
|
|
450
|
+
if (text && !pr.title?.toLowerCase().includes(text) && !pr.description?.toLowerCase().includes(text)) return false;
|
|
451
|
+
return facet.relationship == null || matchesRelationship(pr, facet.relationship, query.currentUser, facet.role);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Whether `facet` is the one that serves `pr`: the first requested relationship the pull request matches, in canonical
|
|
455
|
+
* order. A pull request I authored and reviewed is served by the author facet alone, so the reviewed facet skips it
|
|
456
|
+
* on whichever page it reaches it. Between the two `Reviewed` dashboard facets the reviewer list wins, should a
|
|
457
|
+
* server ever list the user in both.
|
|
458
|
+
*
|
|
459
|
+
* Stateless, which is the point: two facets reach a shared row pages apart, and remembering what earlier pages emitted
|
|
460
|
+
* would make the cursor grow with every row read. The rule only needs the row itself, and the count applies the same
|
|
461
|
+
* union through its own deduplication, so list and count agree. One thing no page-based read can settle is a pull
|
|
462
|
+
* request whose review state changes between two pages — it can then match a different relationship on each and be
|
|
463
|
+
* served by both, just as a row updated mid-read can move between offsets.
|
|
464
|
+
*/
|
|
465
|
+
function ownsRow(pr, facet, query) {
|
|
466
|
+
if (facet.relationship == null) return true;
|
|
467
|
+
const user = query.currentUser;
|
|
468
|
+
if (query.relationships.find((r) => matchesRelationship(pr, r, user)) !== facet.relationship) return false;
|
|
469
|
+
if (facet.role == null) return true;
|
|
470
|
+
return (matchesRelationship(pr, facet.relationship, user, "REVIEWER") ? "REVIEWER" : "PARTICIPANT") === facet.role;
|
|
471
|
+
}
|
|
472
|
+
/** `role` narrows `Reviewed` to one participant list; every other relationship ignores it. */
|
|
473
|
+
function matchesRelationship(pr, relationship, user, role) {
|
|
474
|
+
const isCurrentUser = (candidate) => candidate != null && (String(candidate.id) === user.id || candidate.name?.toLowerCase() === user.username.toLowerCase());
|
|
475
|
+
const hasStatus = (participants, statuses) => participants?.some((p) => isCurrentUser(p.user) && statuses.includes(p.status)) ?? false;
|
|
476
|
+
switch (relationship) {
|
|
477
|
+
case "author": return isCurrentUser(pr.author?.user);
|
|
478
|
+
case "review-requested": return hasStatus(pr.reviewers, ["UNAPPROVED"]);
|
|
479
|
+
case "reviewed-by": return role !== "PARTICIPANT" && hasStatus(pr.reviewers, ["APPROVED", "NEEDS_WORK"]) || role !== "REVIEWER" && hasStatus(pr.participants, ["APPROVED", "NEEDS_WORK"]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
export { countBitbucketServerPullRequests, searchBitbucketServerPullRequestsPage };
|
|
484
|
+
|
|
485
|
+
//# sourceMappingURL=pullRequestSearch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pullRequestSearch.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/bitbucket-server/pullRequestSearch.ts"],"sourcesContent":["import type { CollectionScope, CollectionScopeFailure, CollectionScopeFailureKind } from '@gitkraken/provider-apis';\nimport type {\n\tPullRequestSearchCriteria,\n\tPullRequestSorting,\n\tPullRequestStateFilter,\n} from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort, PullRequestFilter } from '@gitlens/git/models/pullRequest.js';\nimport type { Provider } from '@gitlens/git/models/remoteProvider.js';\nimport { getPullRequestComparator } from '@gitlens/git/utils/pullRequest.utils.js';\nimport { CancellationError, isCancellationError } from '@gitlens/utils/cancellation.js';\nimport { getNonce, sha256 } from '@gitlens/utils/crypto.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { TokenWithInfo } from '../../authentication/models.js';\nimport { toCollectionScopeFailure } from '../../collectionMetadata.js';\nimport type { GitSelfManagedHostIntegrationId } from '../../constants.js';\nimport { AuthenticationError, AuthenticationErrorReason } from '../../errors.js';\nimport type { ProviderPullRequestCount, ProviderPullRequestSearchPage } from '../../models/pullRequestReads.js';\nimport type { ProviderPullRequest, ProviderRepoInput, ProviderRequestFunction } from '../models.js';\nimport { fromProviderPullRequest } from '../models.js';\nimport { throwProviderError } from '../providerErrors.js';\nimport type {\n\tBitbucketServerPagedResponse,\n\tBitbucketServerPullRequest,\n\tBitbucketServerPullRequestUser,\n\tBitbucketServerUser,\n} from './models.js';\nimport { normalizeBitbucketServerPullRequest } from './models.js';\n\ntype Token = TokenWithInfo<GitSelfManagedHostIntegrationId.BitbucketServer>;\ntype BitbucketServerPullRequestState = BitbucketServerPullRequest['state'];\n\n/**\n * The relationships Bitbucket Data Center can express, in the canonical order its facets (and so its cursor) are\n * built in. A Bitbucket pull request has no assignee, and none of its pull-request endpoints filter by mention.\n */\nconst relationships = [\n\tPullRequestFilter.Author,\n\tPullRequestFilter.ReviewRequested,\n\tPullRequestFilter.Reviewed,\n] as const;\ntype Relationship = (typeof relationships)[number];\n\n/** The current user, which both the server-side participant filters and their client-side re-check identify. */\nexport interface BitbucketServerSearchUser {\n\tid: string;\n\tusername: string;\n}\n\n/**\n * One provider query of the search: a repository or the current user's dashboard, times one relationship.\n *\n * A repository facet reads `/projects/{key}/repos/{slug}/pull-requests`, which filters participants, text and draft\n * state server-side, so a page narrowed to a few repositories holds only matches. Without repositories a relationship\n * reads `/dashboard/pull-requests`, the only cross-repository pull-request list Bitbucket Data Center has; it can't\n * filter text or draft state, so those are re-checked on its rows. There is no project-wide list at all, which is\n * why the capability declares no organization scope.\n *\n * States are not a facet axis: every facet asks the server for the one requested state, or for every state when\n * several are requested, and keeps the requested ones — reading the unrequested rows rather than spending one more\n * request per state.\n */\ninterface Facet {\n\trepo?: ProviderRepoInput;\n\trelationship?: Relationship;\n\t/**\n\t * The dashboard role a `Reviewed` facet reads. The dashboard only applies `participantStatus` together with\n\t * `role` — without one it returns every pull request the user is involved in, authored ones included, whatever\n\t * the status — so `Reviewed` is read as two facets, the reviewer list and the (non-reviewer) participant list,\n\t * each filtered by status server-side. Absent for every other facet.\n\t */\n\trole?: DashboardRole;\n}\n\ntype DashboardRole = 'REVIEWER' | 'PARTICIPANT';\n\ninterface SearchOptions {\n\tbaseUrl: string;\n\trepos?: ProviderRepoInput[];\n\t/** Refused when set: Bitbucket Data Center has no project-wide pull-request list to scope it to. */\n\torg?: string;\n\tcriteria?: PullRequestSearchCriteria;\n\t/** Required when `criteria.relationships` is non-empty. */\n\tcurrentUser?: BitbucketServerSearchUser;\n}\n\n/** A resolved query, shared by every facet of one search or count. */\ninterface Query extends SearchOptions {\n\tfacets: Facet[];\n\t/** The requested relationships, in canonical order — the order {@link ownsRow} gives a shared row to. */\n\trelationships: Relationship[];\n\t/** The requested states, or `undefined` for every state. */\n\tstates: Set<BitbucketServerPullRequestState> | undefined;\n\tsort: PullRequestSorting;\n}\n\n/** Bitbucket answers an unspecified page size with 25; a search page asks for this many per facet instead. */\nconst defaultSearchPageSize = 30;\n/**\n * The server's own cap on a page (`page.max.pullrequests`, 1000 unless an administrator lowered it), and the one\n * page a count reads per facet: past it the count is a floor rather than more of a customer-run server's capacity\n * spent on a figure the caller uses as a cost hint.\n */\nconst maxPageSize = 1000;\n/** Matches the SDK's own cap on concurrent Bitbucket Data Center pull-request requests. */\nconst facetConcurrency = 5;\n\n/** The server states each facade state covers. `closed` is two, so it can't be sent as one `state` filter. */\nconst pullRequestStates: Record<Exclude<PullRequestStateFilter, 'all'>, BitbucketServerPullRequestState[]> = {\n\topen: ['OPEN'],\n\tclosed: ['DECLINED', 'SUPERSEDED'],\n\tmerged: ['MERGED'],\n};\n\nconst orderParams: Partial<Record<PullRequestSorting, string>> = {\n\t'updated:desc': 'NEWEST',\n\t'updated:asc': 'OLDEST',\n};\n\n/**\n * Pull requests matching `criteria` over the requested repositories, or over the current user's relationships when\n * none are given.\n *\n * Every facet reads its own page, and the merged page is re-ordered as a whole. Each facet keeps its own\n * `nextPageStart` in the cursor, which is bound to this connection, installation and query: a cursor handed back\n * under anything else is refused rather than read as page 1 of a different search. A pull request several\n * relationships match is served by exactly one of them, the first in canonical order (see {@link ownsRow}), so facets\n * that advance independently never serve it twice across pages and the cursor carries no history of what it emitted.\n *\n * Every criterion the server applies is applied again to the rows it returns, so a server that ignores a parameter\n * (one older than 8.18, which shipped drafts, has no `draft`) narrows the page instead of silently widening it.\n *\n * A facet that fails is reported in `metadata` while its siblings still answer, and keeps its offset, so the next page\n * retries the page it missed — a transient failure recovers the rows it cost. A facet that fails its retry too is\n * dropped, and reported on every later page, which no longer reads it; one retry keeps a facet that will never answer\n * from holding `hasMore` open forever. If every facet fails on the first page, the first failure is thrown instead.\n */\nexport async function searchBitbucketServerPullRequestsPage(\n\trequest: ProviderRequestFunction,\n\ttoken: Token,\n\toptions: SearchOptions & { connectionId: string; provider: Provider; cursor?: string; pageSize?: number },\n\tcancellation?: AbortSignal,\n): Promise<ProviderPullRequestSearchPage> {\n\tconst query = toQuery(options);\n\tconst comparator = getPullRequestComparator(query.sort)!;\n\tconst key = await getCursorKey(options.connectionId, query);\n\tconst cursor = await parseCursor(options.cursor, key, query.facets);\n\tconst active = query.facets.flatMap((facet, index) =>\n\t\tcursor.starts[index] != null ? [{ facet: facet, start: cursor.starts[index], index: index }] : [],\n\t);\n\t// A facet dropped on an earlier page is gone from this one's reads, and the hole is still in the result.\n\tconst failed = [...cursor.failed];\n\tconst failures = failed.map(f => toEarlierFailure(token, query.facets[f.facet], f.kind));\n\tconst retrying: number[] = [];\n\tif (active.length === 0) {\n\t\treturn {\n\t\t\tvalues: [],\n\t\t\thasMore: false,\n\t\t\tpage: cursor.page,\n\t\t\ttruncated: cursor.truncated,\n\t\t\tmetadata: toMetadata(failures),\n\t\t};\n\t}\n\n\tconst limit = Math.min(maxPageSize, Math.max(1, Math.trunc(options.pageSize ?? defaultSearchPageSize)));\n\tconst results = await mapSettledBounded(active, facetConcurrency, ({ facet, start }) =>\n\t\treadFacetPage(request, token, query, facet, start, limit, false, cancellation),\n\t);\n\tconst rejected = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');\n\tconst cancelled = rejected.find(r => isCancellationError(r.reason));\n\tif (cancelled != null) throw cancelled.reason;\n\n\t// A rejected credential is the connection failing rather than one scope, so it is thrown on any page: the\n\t// integration's session recovery then runs once, from the thrown error, and the failure is never carried in the\n\t// cursor where every later page would report it again. (A repository refusing the token isn't one; see\n\t// `toFacetError`.)\n\tconst credential = rejected.find(r => r.reason instanceof AuthenticationError);\n\tif (credential != null) throw credential.reason;\n\t// Only a first page on which every facet failed is a failed read. On a later page an earlier one already proved\n\t// some facet answered, so the rows it served stand and this page's failures join the partial-result report.\n\tif (cursor.page === 1 && rejected.length === results.length) throw rejected[0].reason;\n\n\tconst nextStarts: (number | null)[] = cursor.starts.map(() => null);\n\tconst values: ProviderPullRequest[] = [];\n\tlet truncated = cursor.truncated;\n\tfor (let i = 0; i < results.length; i++) {\n\t\tconst { facet, index } = active[i];\n\t\tconst result = results[i];\n\t\tif (result.status === 'rejected') {\n\t\t\tconst failure = toCollectionScopeFailure(toCollectionScope(token, facet), result.reason);\n\t\t\tfailures.push(failure);\n\t\t\tif (cursor.retrying.includes(index)) {\n\t\t\t\t// Its retry failed too: dropped, and carried so every later page still reports the hole.\n\t\t\t\tfailed.push({ facet: index, kind: failure.kind });\n\t\t\t} else {\n\t\t\t\t// Kept at the offset it missed, so the next page retries it.\n\t\t\t\tnextStarts[index] = active[i].start;\n\t\t\t\tretrying.push(index);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tnextStarts[index] = result.value.next;\n\t\ttruncated ||= result.value.stalled;\n\t\tfor (const { raw, normalized } of result.value.matches) {\n\t\t\t// Another relationship owns a row they both match, and serves it from its own facet on whichever page\n\t\t\t// reaches it — which also settles two facets reaching it on this one.\n\t\t\tif (!ownsRow(raw, facet, query)) continue;\n\n\t\t\tvalues.push(normalized);\n\t\t}\n\t}\n\n\tconst hasMore = nextStarts.some(start => start != null);\n\t// Written on every continuation, complete or not, so removing it can't pass an incomplete read off as complete.\n\tconst state = { failed: failed, retrying: retrying, truncated: truncated };\n\treturn {\n\t\tvalues: values\n\t\t\t.map(pr => fromProviderPullRequest(pr, options.provider, { currentAccountId: options.currentUser?.id }))\n\t\t\t.sort(comparator),\n\t\tcursor: hasMore\n\t\t\t? JSON.stringify({\n\t\t\t\t\tkey: key,\n\t\t\t\t\tpage: cursor.page + 1,\n\t\t\t\t\tstarts: nextStarts,\n\t\t\t\t\tstate: { ...state, signature: await signState(key, cursor.page + 1, nextStarts, state) },\n\t\t\t\t})\n\t\t\t: undefined,\n\t\thasMore: hasMore,\n\t\tpage: cursor.page,\n\t\ttruncated: truncated,\n\t\tmetadata: toMetadata(failures),\n\t};\n}\n\nfunction toMetadata(failures: CollectionScopeFailure[]): ProviderPullRequestSearchPage['metadata'] {\n\treturn failures.length > 0 ? { completeness: 'partial', failures: failures } : undefined;\n}\n\n/**\n * A facet failure carried over from an earlier page, rebuilt from the query rather than from the cursor: the cursor\n * records only which facet failed and how, so it can't name a scope or a message the read never produced.\n */\nfunction toEarlierFailure(token: Token, facet: Facet, kind: CollectionScopeFailureKind): CollectionScopeFailure {\n\treturn {\n\t\tscope: toCollectionScope(token, facet),\n\t\tkind: kind,\n\t\tmessage: 'Failed on an earlier page of this search, so later pages did not read it',\n\t};\n}\n\n/**\n * How many pull requests match `criteria`: the same facets, predicates and identity the search uses, so the count\n * is the number of distinct rows the search would return — the union of the requested states, not the largest of\n * them, since Bitbucket has no per-search ceiling for a maximum to stay under.\n *\n * Bitbucket Data Center has no count query and reports no total, so each facet reads one page of up to\n * {@link maxPageSize} and the count is a floor (`lowerBound`) when any facet had more. Unlike the search, one failed\n * facet fails the whole count: a count missing a facet is a wrong number rather than a partial one.\n */\nexport async function countBitbucketServerPullRequests(\n\trequest: ProviderRequestFunction,\n\ttoken: Token,\n\toptions: SearchOptions,\n\tcancellation?: AbortSignal,\n): Promise<ProviderPullRequestCount> {\n\tconst query = toQuery(options);\n\tconst seen = new Set<string>();\n\tlet lowerBound = false;\n\t// One facet at a time: the facade already counts several scopes concurrently.\n\tfor (const facet of query.facets) {\n\t\tconst result = await readFacetPage(request, token, query, facet, 0, maxPageSize, true, cancellation);\n\t\tfor (const { raw } of result.matches) {\n\t\t\tseen.add(toIdentity(raw));\n\t\t}\n\t\tlowerBound ||= result.next != null || result.stalled;\n\t}\n\n\treturn { count: seen.size, lowerBound: lowerBound || undefined };\n}\n\n/**\n * Resolves the criteria into the facets and predicates every page applies, refusing — by throwing, which the read\n * reports as a failed request — anything the capability table does not declare. The facade validates the same\n * table first, so reaching a refusal here means a caller bypassed it; refusing rather than dropping the criterion\n * keeps that caller from reading a wider set than it asked for.\n */\nfunction toQuery(options: SearchOptions): Query {\n\tif (options.org != null && options.org.length > 0) {\n\t\tthrow new Error('Bitbucket Data Center cannot scope a pull request search to a project');\n\t}\n\n\tconst criteria = options.criteria;\n\t// No endpoint filters by date, and filtering the rows read would page through everything before the cutoff.\n\tif (criteria?.updatedAfter != null || criteria?.createdAfter != null) {\n\t\tthrow new Error('Bitbucket Data Center cannot filter a pull request search by date');\n\t}\n\n\tconst sort = criteria?.sort ?? defaultPullRequestSort;\n\tif (orderParams[sort] == null) {\n\t\tthrow new Error(`Bitbucket Data Center cannot order a pull request search by '${sort}'`);\n\t}\n\n\tconst requested = criteria?.relationships ?? [];\n\tconst unsupported = requested.find(r => !(relationships as readonly PullRequestFilter[]).includes(r));\n\tif (unsupported != null) {\n\t\tthrow new Error(`Bitbucket Data Center cannot search pull requests by the '${unsupported}' relationship`);\n\t}\n\n\tconst facetRelationships = relationships.filter(r => requested.includes(r));\n\tif (facetRelationships.length > 0 && options.currentUser == null) {\n\t\tthrow new Error('A Bitbucket Data Center relationship search requires the current user');\n\t}\n\n\tconst requestedStates: PullRequestStateFilter[] = criteria?.states?.length ? criteria.states : ['open'];\n\tconst states = requestedStates.includes('all')\n\t\t? undefined\n\t\t: new Set(requestedStates.flatMap(s => pullRequestStates[s as Exclude<PullRequestStateFilter, 'all'>]));\n\n\t// Sorted and deduplicated so the facets, and so the cursor, don't depend on the order repositories were passed.\n\tconst repos = [...new Map((options.repos ?? []).map(r => [repoKey(r), r])).entries()]\n\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t.map(([, repo]) => repo);\n\tfor (const repo of repos) {\n\t\tfor (const segment of [repo.namespace, repo.name]) {\n\t\t\tif (!segment || segment === '.' || segment === '..') {\n\t\t\t\tthrow new Error('Invalid Bitbucket Data Center repository for a pull request search');\n\t\t\t}\n\t\t}\n\t}\n\n\tlet facets: Facet[];\n\tif (repos.length > 0) {\n\t\tconst scoped = facetRelationships.length > 0 ? facetRelationships : [undefined];\n\t\tfacets = repos.flatMap(repo => scoped.map(relationship => ({ repo: repo, relationship: relationship })));\n\t} else if (facetRelationships.length > 0) {\n\t\tfacets = facetRelationships.flatMap<Facet>(relationship =>\n\t\t\trelationship === PullRequestFilter.Reviewed\n\t\t\t\t? [\n\t\t\t\t\t\t{ relationship: relationship, role: 'REVIEWER' },\n\t\t\t\t\t\t{ relationship: relationship, role: 'PARTICIPANT' },\n\t\t\t\t\t]\n\t\t\t\t: [{ relationship: relationship }],\n\t\t);\n\t} else {\n\t\tthrow new Error('A Bitbucket Data Center pull request search must be scoped by repositories or a relationship');\n\t}\n\n\treturn { ...options, facets: facets, relationships: facetRelationships, states: states, sort: sort };\n}\n\n/** Project keys and repository slugs resolve case-insensitively, so two spellings name one repository. */\nfunction repoKey(repo: ProviderRepoInput): string {\n\treturn `${repo.namespace}/${repo.name}`.toLowerCase();\n}\n\n/**\n * A facet's classified failure, with the one correction the provider-wide classification can't make: a 403 on a\n * REPOSITORY is Bitbucket refusing that repository (the token lacks `REPO_READ` there), not the credential failing.\n *\n * Made here, where every read of a facet passes, so the search and the count can't disagree about it on any path —\n * a sibling repository's failure, a first page where every repository failed, or a count. Left an\n * `AuthenticationError`, it would reach the integration's exception handling and mark a connection that works\n * everywhere else for recovery and toward disconnecting; as a plain error naming the repository it is reported as\n * that scope's failure alone. A 401 anywhere, and a 403 on the user's own dashboard, stay the credential failing.\n */\nfunction toFacetError(ex: unknown, facet: Facet): unknown {\n\tif (!(ex instanceof AuthenticationError) || facet.repo == null) return ex;\n\tif (ex.reason !== AuthenticationErrorReason.Forbidden) return ex;\n\n\treturn new Error(\n\t\t`Bitbucket Data Center refused access to ${facet.repo.namespace}/${facet.repo.name}: ${(ex.original ?? ex).message}`,\n\t\t{ cause: ex },\n\t);\n}\n\n/** A pull request's identity across every facet and endpoint: its id is only unique within its repository. */\nfunction toIdentity(pr: BitbucketServerPullRequest): string {\n\treturn `${pr.toRef.repository.id}/${pr.id}`;\n}\n\n/**\n * Binds a cursor to everything that decides what it continues: the connection and installation it was read from,\n * the facets and every criterion. Hashed, so neither the query nor the address is published inside the cursor.\n */\nfunction getCursorKey(connectionId: string, query: Query): Promise<string> {\n\treturn sha256(\n\t\tJSON.stringify([\n\t\t\tconnectionId,\n\t\t\tquery.baseUrl,\n\t\t\tquery.facets.map(f => [f.repo != null ? repoKey(f.repo) : null, f.relationship ?? null, f.role ?? null]),\n\t\t\tquery.states != null ? [...query.states].sort() : null,\n\t\t\tquery.criteria?.text?.trim() ?? '',\n\t\t\tquery.criteria?.draft ?? null,\n\t\t\tquery.criteria?.includeArchived === true,\n\t\t\t// Both: the server filters participants by username and the rows are re-checked by id or username, so a\n\t\t\t// renamed account is a different query even under the same id.\n\t\t\tquery.currentUser != null ? [query.currentUser.id, query.currentUser.username] : null,\n\t\t\tquery.sort,\n\t\t]),\n\t);\n}\n\n/**\n * Signs a continuation — its page, every facet's offset, which facets failed, and whether a facet stalled — so an\n * edited cursor can't pass for complete: ending a facet early by nulling its offset, or claiming a failure its read\n * never had. The key only binds a cursor to its query and travels inside it; this is keyed by a secret that never\n * leaves this process, so a signature can't be recomputed outside it. A cursor that outlives the process keeps\n * continuing, and reports its read as unconfirmed rather than as complete (see {@link parseCursor}).\n */\nconst stateSecret = getNonce();\n\nfunction signState(\n\tkey: string,\n\tpage: number,\n\tstarts: (number | null)[],\n\tstate: Pick<SearchCursor, 'failed' | 'retrying' | 'truncated'>,\n): Promise<string> {\n\treturn sha256(JSON.stringify([stateSecret, key, page, starts, state.failed, state.retrying, state.truncated]));\n}\n\n/** The failure kinds a cursor may carry — the SDK's closed set, so a cursor can't name one the facade never maps. */\nconst failureKinds: ReadonlySet<string> = new Set<CollectionScopeFailureKind>([\n\t'authentication',\n\t'rate-limit',\n\t'not-found',\n\t'network',\n\t'provider',\n\t'unknown',\n]);\n\ninterface SearchCursor {\n\tpage: number;\n\tstarts: (number | null)[];\n\ttruncated: boolean;\n\t/** The facets dropped after failing their retry, by index, and how. They are never read again. */\n\tfailed: { facet: number; kind: CollectionScopeFailureKind }[];\n\t/** The facets that failed on the previous page and are retried at the offset they missed, by index. */\n\tretrying: number[];\n}\n\n/**\n * Parses a cursor this module wrote for the same connection and query.\n *\n * The key binds a cursor to its connection, installation and query, but travels inside it, so the offsets can be\n * edited; the signature below covers them, so an edited cursor still reads but can't report its read as complete. The completeness state is\n * the part that could be turned against it, so it is signed and written on every continuation. A state that is\n * missing or doesn't verify is not refused, since that is also what a cursor from an earlier process looks like, and\n * failing a read that still works helps no one; but it can't be passed off as complete either — it degrades to\n * `truncated`, \"completeness not confirmed\", without the failures it can't prove. A failure is rebuilt from the facet\n * it names regardless, so even a verified one can't name a scope or text the read never produced.\n */\nasync function parseCursor(cursor: string | undefined, key: string, facets: Facet[]): Promise<SearchCursor> {\n\tconst facetCount = facets.length;\n\tif (cursor == null || cursor === '{}') {\n\t\treturn {\n\t\t\tpage: 1,\n\t\t\tstarts: new Array<number | null>(facetCount).fill(0),\n\t\t\ttruncated: false,\n\t\t\tfailed: [],\n\t\t\tretrying: [],\n\t\t};\n\t}\n\n\tlet parsed: { key?: unknown; page?: unknown; starts?: unknown; state?: unknown } | undefined;\n\ttry {\n\t\tparsed = (JSON.parse(cursor) as typeof parsed) ?? undefined;\n\t} catch {}\n\n\tconst starts = parsed?.starts;\n\tif (\n\t\tparsed == null ||\n\t\tparsed.key !== key ||\n\t\ttypeof parsed.page !== 'number' ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page < 2 ||\n\t\t!Array.isArray(starts) ||\n\t\tstarts.length !== facetCount ||\n\t\t// 0 is a facet retrying the first page it missed; every other offset is past a page already read.\n\t\t!starts.every(start => start === null || (Number.isSafeInteger(start) && (start as number) >= 0)) ||\n\t\t!starts.some(start => start !== null)\n\t) {\n\t\tthrow new Error('Invalid Bitbucket Data Center pull request search cursor for this connection or query');\n\t}\n\n\treturn {\n\t\tpage: parsed.page,\n\t\tstarts: starts as (number | null)[],\n\t\t...(await parseState(parsed.state, key, parsed.page, starts as (number | null)[])),\n\t};\n}\n\n/**\n * The signed completeness state of a continuation, verified together with its page and offsets. Every continuation\n * carries one, so a state that is missing, malformed, not signed by this process, or signed for other offsets says\n * the read's completeness can't be confirmed — never that it was whole.\n */\nasync function parseState(\n\tstate: unknown,\n\tkey: string,\n\tpage: number,\n\tstarts: (number | null)[],\n): Promise<Pick<SearchCursor, 'failed' | 'retrying' | 'truncated'>> {\n\tconst unconfirmed = { failed: [], retrying: [], truncated: true };\n\tif (state == null || typeof state !== 'object') return unconfirmed;\n\n\tconst { failed, retrying, truncated, signature } = state as {\n\t\tfailed?: unknown;\n\t\tretrying?: unknown;\n\t\ttruncated?: unknown;\n\t\tsignature?: unknown;\n\t};\n\tif (\n\t\ttypeof truncated !== 'boolean' ||\n\t\t!Array.isArray(retrying) ||\n\t\t// A retried facet is read again, so it must still have the offset it missed.\n\t\t!retrying.every(i => Number.isSafeInteger(i) && i >= 0 && i < starts.length && starts[i as number] !== null) ||\n\t\t!Array.isArray(failed) ||\n\t\t!failed.every(\n\t\t\t(f: { facet?: unknown; kind?: unknown } | null) =>\n\t\t\t\tf != null &&\n\t\t\t\tNumber.isSafeInteger(f.facet) &&\n\t\t\t\t(f.facet as number) >= 0 &&\n\t\t\t\t(f.facet as number) < starts.length &&\n\t\t\t\t// A failed facet is never read again, so it has no offset left to continue from.\n\t\t\t\tstarts[f.facet as number] === null &&\n\t\t\t\ttypeof f.kind === 'string' &&\n\t\t\t\tfailureKinds.has(f.kind),\n\t\t)\n\t) {\n\t\treturn unconfirmed;\n\t}\n\n\tconst verified = { failed: failed as SearchCursor['failed'], retrying: retrying as number[], truncated: truncated };\n\treturn signature === (await signState(key, page, starts, verified)) ? verified : unconfirmed;\n}\n\nfunction toCollectionScope(token: Token, facet: Facet): CollectionScope {\n\treturn facet.repo != null\n\t\t? { providerId: token.providerId, repositoryId: `${facet.repo.namespace}/${facet.repo.name}` }\n\t\t: { providerId: token.providerId };\n}\n\ninterface FacetPage {\n\t/**\n\t * The rows that match the query AND map to a pull request, with their mapping. Mapped here rather than by each\n\t * caller, so the count skips exactly the rows the search can't show and the two can't disagree about them.\n\t */\n\tmatches: { raw: BitbucketServerPullRequest; normalized: ProviderPullRequest }[];\n\t/** The next page's `start`, or `null` when this facet is exhausted or can't be continued. */\n\tnext: number | null;\n\t/** The server reported another page but no offset past this one to reach it. */\n\tstalled: boolean;\n}\n\nasync function readFacetPage(\n\trequest: ProviderRequestFunction,\n\ttoken: Token,\n\tquery: Query,\n\tfacet: Facet,\n\tstart: number,\n\tlimit: number,\n\tcountOnly: boolean,\n\tcancellation: AbortSignal | undefined,\n): Promise<FacetPage> {\n\tif (cancellation?.aborted) throw new CancellationError();\n\n\tlet body: BitbucketServerPagedResponse<BitbucketServerPullRequest> | undefined;\n\ttry {\n\t\t({ body } = await request<BitbucketServerPagedResponse<BitbucketServerPullRequest>>({\n\t\t\turl: toFacetUrl(query, facet, start, limit, countOnly),\n\t\t\theaders: { Authorization: `Bearer ${token.accessToken}` },\n\t\t\t// Aborts the request itself, so a cancelled search doesn't wait on up to a page of 1,000 per facet.\n\t\t\tsignal: cancellation,\n\t\t}));\n\t\tif (\n\t\t\tbody == null ||\n\t\t\t!Array.isArray(body.values) ||\n\t\t\ttypeof body.isLastPage !== 'boolean' ||\n\t\t\tbody.start !== start\n\t\t) {\n\t\t\tthrow new Error('Invalid Bitbucket Data Center pull request page');\n\t\t}\n\t} catch (ex) {\n\t\t// A request the signal aborted is the cancellation, never a failed facet whose siblings answered.\n\t\tif (isCancellationError(ex)) throw ex;\n\t\tif (cancellation?.aborted) throw new CancellationError(ex instanceof Error ? ex : undefined);\n\n\t\ttry {\n\t\t\tthrowProviderError(token, ex);\n\t\t} catch (classified) {\n\t\t\tthrow toFacetError(classified, facet);\n\t\t}\n\t}\n\tif (cancellation?.aborted) throw new CancellationError();\n\n\tconst next = body.nextPageStart;\n\tconst more = !body.isLastPage && Number.isSafeInteger(next) && next > start;\n\tconst matches: FacetPage['matches'] = [];\n\tfor (const raw of body.values) {\n\t\tif (!matchesFacet(raw, facet, query)) continue;\n\n\t\ttry {\n\t\t\tmatches.push({ raw: raw, normalized: normalizeBitbucketServerPullRequest(raw) });\n\t\t} catch (ex) {\n\t\t\tLogger.warn(`Skipped an unmappable Bitbucket Data Center pull request; id=${raw.id}, ex=${String(ex)}`);\n\t\t}\n\t}\n\n\treturn {\n\t\tmatches: matches,\n\t\tnext: more ? next : null,\n\t\tstalled: !body.isLastPage && !more,\n\t};\n}\n\nfunction toFacetUrl(query: Query, facet: Facet, start: number, limit: number, countOnly: boolean): string {\n\tconst criteria = query.criteria;\n\t// One requested state goes to the server; several — including `closed`, which is DECLINED and SUPERSEDED — are\n\t// read as every state and kept by `matchesFacet`.\n\tconst state = query.states?.size === 1 ? [...query.states][0] : undefined;\n\tlet url: URL;\n\tif (facet.repo != null) {\n\t\turl = new URL(\n\t\t\t`${query.baseUrl}/projects/${encodeURIComponent(facet.repo.namespace)}/repos/${encodeURIComponent(\n\t\t\t\tfacet.repo.name,\n\t\t\t)}/pull-requests`,\n\t\t);\n\t\turl.searchParams.set('state', state ?? 'ALL');\n\t\tconst text = criteria?.text?.trim();\n\t\tif (text) {\n\t\t\turl.searchParams.set('filterText', text);\n\t\t}\n\t\tif (criteria?.draft != null) {\n\t\t\turl.searchParams.set('draft', String(criteria.draft));\n\t\t}\n\t\tif (facet.relationship != null) {\n\t\t\turl.searchParams.set('username.1', query.currentUser!.username);\n\t\t\tswitch (facet.relationship) {\n\t\t\t\tcase PullRequestFilter.Author:\n\t\t\t\t\turl.searchParams.set('role.1', 'AUTHOR');\n\t\t\t\t\tbreak;\n\t\t\t\tcase PullRequestFilter.ReviewRequested:\n\t\t\t\t\turl.searchParams.set('role.1', 'REVIEWER');\n\t\t\t\t\turl.searchParams.set('approved.1', 'false');\n\t\t\t\t\tbreak;\n\t\t\t\tcase PullRequestFilter.Reviewed:\n\t\t\t\t\t// Any role: a participant who isn't a listed reviewer can still approve or ask for changes.\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t// Properties carry the comment count a search row shows and a count never reads.\n\t\tif (countOnly) {\n\t\t\turl.searchParams.set('withProperties', 'false');\n\t\t}\n\t} else {\n\t\turl = new URL(`${query.baseUrl}/dashboard/pull-requests`);\n\t\t// The dashboard reads every state when `state` is omitted, and documents no `ALL`.\n\t\tif (state != null) {\n\t\t\turl.searchParams.set('state', state);\n\t\t}\n\t\tswitch (facet.relationship) {\n\t\t\tcase PullRequestFilter.Author:\n\t\t\t\turl.searchParams.set('role', 'AUTHOR');\n\t\t\t\tbreak;\n\t\t\tcase PullRequestFilter.ReviewRequested:\n\t\t\t\turl.searchParams.set('role', 'REVIEWER');\n\t\t\t\turl.searchParams.set('participantStatus', 'UNAPPROVED');\n\t\t\t\tbreak;\n\t\t\tcase PullRequestFilter.Reviewed:\n\t\t\t\t// The status only filters alongside a role (see `Facet.role`), so each of the two lists is its own facet.\n\t\t\t\turl.searchParams.set('role', facet.role!);\n\t\t\t\turl.searchParams.set('participantStatus', 'APPROVED,NEEDS_WORK');\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\turl.searchParams.set('order', orderParams[query.sort]!);\n\turl.searchParams.set('start', String(start));\n\turl.searchParams.set('limit', String(limit));\n\treturn url.toString();\n}\n\n/**\n * Re-applies every constraint of the query to a row the server returned, so what the server was asked to filter and\n * what it actually filtered can't differ in the result: the dashboard has no text or draft filter, a server older\n * than 8.18 ignores `draft`, several states are read as every state, a reviewer filter can't tell a pending review\n * from one that asked for changes, and no endpoint excludes archived repositories.\n */\nfunction matchesFacet(pr: BitbucketServerPullRequest, facet: Facet, query: Query): boolean {\n\tconst repository = pr.toRef?.repository;\n\t// Without both there is no identity to deduplicate or count it by.\n\tif (pr.id == null || repository?.id == null) return false;\n\tif (query.states != null && !query.states.has(pr.state)) return false;\n\tif (facet.repo != null && `${repository.project?.key}/${repository.slug}`.toLowerCase() !== repoKey(facet.repo)) {\n\t\treturn false;\n\t}\n\n\tconst criteria = query.criteria;\n\tif (criteria?.includeArchived !== true && repository.archived === true) return false;\n\tif (criteria?.draft != null && (pr.draft === true) !== criteria.draft) return false;\n\n\tconst text = criteria?.text?.trim().toLowerCase();\n\tif (text && !pr.title?.toLowerCase().includes(text) && !pr.description?.toLowerCase().includes(text)) {\n\t\treturn false;\n\t}\n\n\treturn facet.relationship == null || matchesRelationship(pr, facet.relationship, query.currentUser!, facet.role);\n}\n\n/**\n * Whether `facet` is the one that serves `pr`: the first requested relationship the pull request matches, in canonical\n * order. A pull request I authored and reviewed is served by the author facet alone, so the reviewed facet skips it\n * on whichever page it reaches it. Between the two `Reviewed` dashboard facets the reviewer list wins, should a\n * server ever list the user in both.\n *\n * Stateless, which is the point: two facets reach a shared row pages apart, and remembering what earlier pages emitted\n * would make the cursor grow with every row read. The rule only needs the row itself, and the count applies the same\n * union through its own deduplication, so list and count agree. One thing no page-based read can settle is a pull\n * request whose review state changes between two pages — it can then match a different relationship on each and be\n * served by both, just as a row updated mid-read can move between offsets.\n */\nfunction ownsRow(pr: BitbucketServerPullRequest, facet: Facet, query: Query): boolean {\n\tif (facet.relationship == null) return true;\n\n\tconst user = query.currentUser!;\n\tconst owner = query.relationships.find(r => matchesRelationship(pr, r, user));\n\tif (owner !== facet.relationship) return false;\n\tif (facet.role == null) return true;\n\n\tconst ownerRole: DashboardRole = matchesRelationship(pr, facet.relationship, user, 'REVIEWER')\n\t\t? 'REVIEWER'\n\t\t: 'PARTICIPANT';\n\treturn ownerRole === facet.role;\n}\n\n/** `role` narrows `Reviewed` to one participant list; every other relationship ignores it. */\nfunction matchesRelationship(\n\tpr: BitbucketServerPullRequest,\n\trelationship: Relationship,\n\tuser: BitbucketServerSearchUser,\n\trole?: DashboardRole,\n): boolean {\n\tconst isCurrentUser = (candidate: BitbucketServerUser | undefined): boolean =>\n\t\tcandidate != null &&\n\t\t(String(candidate.id) === user.id || candidate.name?.toLowerCase() === user.username.toLowerCase());\n\tconst hasStatus = (participants: BitbucketServerPullRequestUser[] | undefined, statuses: string[]): boolean =>\n\t\tparticipants?.some(p => isCurrentUser(p.user) && statuses.includes(p.status)) ?? false;\n\n\tswitch (relationship) {\n\t\tcase PullRequestFilter.Author:\n\t\t\treturn isCurrentUser(pr.author?.user);\n\t\tcase PullRequestFilter.ReviewRequested:\n\t\t\treturn hasStatus(pr.reviewers, ['UNAPPROVED']);\n\t\tcase PullRequestFilter.Reviewed:\n\t\t\treturn (\n\t\t\t\t(role !== 'PARTICIPANT' && hasStatus(pr.reviewers, ['APPROVED', 'NEEDS_WORK'])) ||\n\t\t\t\t(role !== 'REVIEWER' && hasStatus(pr.participants, ['APPROVED', 'NEEDS_WORK']))\n\t\t\t);\n\t}\n}\n"],"mappings":"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"}
|