@gitkraken/core-gitlens 0.5.114 → 0.5.116
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 +38 -1
- package/dist/plus/git-github/api/github.d.ts +3 -2
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +37 -37
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +2 -0
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +17 -1
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/context.d.ts +4 -1
- package/dist/plus/integrations/context.d.ts.map +1 -1
- package/dist/plus/integrations/index.d.ts +1 -0
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +9 -1
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +8 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +65 -6
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -26
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +92 -47
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +9 -0
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +52 -19
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueCache.d.ts +16 -0
- package/dist/plus/integrations/models/issueCache.d.ts.map +1 -0
- package/dist/plus/integrations/models/issueCache.js +9 -0
- package/dist/plus/integrations/models/issueCache.js.map +1 -0
- package/dist/plus/integrations/models/issueReads.d.ts +13 -0
- package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +11 -13
- package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +15 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -5
- package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js +9 -8
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -5
- package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +3 -5
- package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.js +2 -1
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +3 -5
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +3 -3
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +3 -5
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +2 -2
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +3 -7
- package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
- package/dist/plus/integrations/providers/jira.js +35 -22
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +7 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts.map +1 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +102 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -0
- package/dist/plus/integrations/providers/linear.d.ts +3 -7
- package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
- package/dist/plus/integrations/providers/linear.js +23 -22
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +3 -1
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +10 -4
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +7 -0
- package/dist/plus/integrations/providers/providerErrors.d.ts.map +1 -0
- package/dist/plus/integrations/providers/providerErrors.js +91 -0
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -0
- package/dist/plus/integrations/providers/providersApi.d.ts +3 -2
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +18 -52
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.d.ts +2 -7
- package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
- package/dist/plus/integrations/providers/trello.js +3 -1
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +2 -0
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +12 -7
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +26 -14
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
- package/dist/plus/integrations/reads/counts.js +39 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +33 -11
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +114 -25
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +23 -17
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.d.ts +1 -5
- package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +12 -6
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.d.ts +1 -0
- package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +8 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +21 -0
- package/dist/plus/integrations/reads/trackerIssue.d.ts.map +1 -0
- package/dist/plus/integrations/reads/trackerIssue.js +51 -0
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -0
- package/dist/plus/integrations/reads/warnings.d.ts +18 -15
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +37 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +4 -20
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/utils/promise.d.ts +2 -0
- package/dist/utils/promise.d.ts.map +1 -1
- package/dist/utils/promise.js +11 -0
- package/dist/utils/promise.js.map +1 -1
- package/docs/integrations.md +63 -28
- package/docs/kepler-read-api-parity.md +13 -0
- package/package.json +2 -2
- package/src/plus/git-github/api/github.ts +54 -60
- package/src/plus/integrations/collectionMetadata.ts +23 -1
- package/src/plus/integrations/context.ts +1 -1
- package/src/plus/integrations/index.ts +1 -0
- package/src/plus/integrations/integrationService.ts +14 -4
- package/src/plus/integrations/manager.ts +65 -6
- package/src/plus/integrations/models/gitHostIntegration.ts +117 -129
- package/src/plus/integrations/models/integration.ts +56 -22
- package/src/plus/integrations/models/issueCache.ts +29 -0
- package/src/plus/integrations/models/issueReads.ts +15 -0
- package/src/plus/integrations/models/issuesIntegration.ts +38 -8
- package/src/plus/integrations/providers/azureDevOps.ts +16 -13
- package/src/plus/integrations/providers/bitbucket-server.ts +4 -6
- package/src/plus/integrations/providers/bitbucket.ts +3 -4
- package/src/plus/integrations/providers/github.ts +5 -7
- package/src/plus/integrations/providers/gitlab.ts +4 -6
- package/src/plus/integrations/providers/jira.ts +52 -26
- package/src/plus/integrations/providers/jiraIssueByKey.ts +155 -0
- package/src/plus/integrations/providers/linear.ts +38 -25
- package/src/plus/integrations/providers/models.ts +17 -5
- package/src/plus/integrations/providers/providerErrors.ts +115 -0
- package/src/plus/integrations/providers/providersApi.ts +26 -68
- package/src/plus/integrations/providers/trello.ts +5 -4
- package/src/plus/integrations/providers/utils/providerPaging.ts +14 -7
- package/src/plus/integrations/reads/broaden.ts +35 -16
- package/src/plus/integrations/reads/counts.ts +62 -15
- package/src/plus/integrations/reads/filters.ts +131 -29
- package/src/plus/integrations/reads/issueTracker.ts +26 -24
- package/src/plus/integrations/reads/searchIssues.ts +23 -13
- package/src/plus/integrations/reads/searchPullRequests.ts +16 -2
- package/src/plus/integrations/reads/trackerIssue.ts +105 -0
- package/src/plus/integrations/reads/warnings.ts +42 -17
- package/src/plus/integrations/results.ts +4 -20
- package/src/utils/promise.ts +15 -0
|
@@ -129,14 +129,19 @@ export function resolvePullRequestSearchCriteria(
|
|
|
129
129
|
|
|
130
130
|
/** Why a filtered pull-request search's repository/organization boundary was refused. */
|
|
131
131
|
export type PullRequestSearchScopeRejection =
|
|
132
|
-
| 'unscoped'
|
|
133
|
-
| 'repo-ids'
|
|
134
|
-
| 'unsupported-repository-scope'
|
|
135
|
-
| 'unsupported-organization-scope'
|
|
132
|
+
| { reason: 'unscoped' }
|
|
133
|
+
| { reason: 'repo-ids' }
|
|
134
|
+
| { reason: 'unsupported-repository-scope' }
|
|
135
|
+
| { reason: 'unsupported-organization-scope' }
|
|
136
|
+
/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */
|
|
137
|
+
| { reason: 'unusable-scope'; scopes: string[] };
|
|
136
138
|
|
|
137
139
|
/**
|
|
138
140
|
* Validates the search boundary independently from its criteria. A current-user relationship is itself a safe
|
|
139
141
|
* account-wide boundary; without one, a repository or organization scope is mandatory.
|
|
142
|
+
*
|
|
143
|
+
* Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather
|
|
144
|
+
* than re-derived, because the defect is identical on both reads.
|
|
140
145
|
*/
|
|
141
146
|
export function resolvePullRequestSearchScope(
|
|
142
147
|
id: IntegrationIds,
|
|
@@ -148,22 +153,32 @@ export function resolvePullRequestSearchScope(
|
|
|
148
153
|
let resolvedRepos: ProviderRepoInput[] | undefined;
|
|
149
154
|
|
|
150
155
|
if (repos?.length) {
|
|
151
|
-
if (repos.some(r => typeof r === 'string' || typeof r === 'number'))
|
|
152
|
-
|
|
156
|
+
if (repos.some(r => typeof r === 'string' || typeof r === 'number')) {
|
|
157
|
+
return { rejection: { reason: 'repo-ids' } };
|
|
158
|
+
}
|
|
159
|
+
if (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };
|
|
153
160
|
|
|
154
161
|
resolvedRepos = repos as ProviderRepoInput[];
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
const hasOrganizationScope = org != null && org.length > 0;
|
|
158
165
|
if (hasOrganizationScope && supported?.organizationScope !== true) {
|
|
159
|
-
return { rejection: 'unsupported-organization-scope' };
|
|
166
|
+
return { rejection: { reason: 'unsupported-organization-scope' } };
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
// After the capability checks and before the "is it scoped at all" rule: a scope the provider cannot express
|
|
170
|
+
// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a
|
|
171
|
+
// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-
|
|
172
|
+
// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like
|
|
173
|
+
// they differ.
|
|
174
|
+
const unusable = unusableSearchScopeNames(org, resolvedRepos);
|
|
175
|
+
if (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };
|
|
176
|
+
|
|
162
177
|
if (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {
|
|
163
178
|
return { repos: resolvedRepos };
|
|
164
179
|
}
|
|
165
180
|
|
|
166
|
-
return { rejection: 'unscoped' };
|
|
181
|
+
return { rejection: { reason: 'unscoped' } };
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
/**
|
|
@@ -321,25 +336,105 @@ const unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities =
|
|
|
321
336
|
/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */
|
|
322
337
|
export type IssueSearchScopeRejection =
|
|
323
338
|
/** No repositories, no org, and no user-relative relationship: a search of the whole host. */
|
|
324
|
-
| 'unscoped'
|
|
339
|
+
| { reason: 'unscoped' }
|
|
325
340
|
/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */
|
|
326
|
-
| 'repo-ids'
|
|
341
|
+
| { reason: 'repo-ids' }
|
|
342
|
+
/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */
|
|
343
|
+
| { reason: 'unusable-scope'; scopes: string[] };
|
|
327
344
|
|
|
328
345
|
/**
|
|
329
|
-
* Whether a scope name
|
|
346
|
+
* Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`
|
|
347
|
+
* refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.
|
|
348
|
+
*
|
|
349
|
+
* The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is
|
|
350
|
+
* built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no
|
|
351
|
+
* longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a
|
|
352
|
+
* consumer to branch on:
|
|
353
|
+
* - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is
|
|
354
|
+
* also what makes a relationship-less read legal, so the request carries neither and every item on the host
|
|
355
|
+
* matches: measured at 52 million issues across unrelated accounts.
|
|
356
|
+
* - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git"kraken` -> `gitkraken`),
|
|
357
|
+
* whose answer looks entirely normal.
|
|
358
|
+
* - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally
|
|
359
|
+
* filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a
|
|
360
|
+
* widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.
|
|
361
|
+
*
|
|
362
|
+
* Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the
|
|
363
|
+
* qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than
|
|
364
|
+
* disagree with it, and a consumer cross-checking "N matched" cannot detect it by construction. Nor can it
|
|
365
|
+
* pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the
|
|
366
|
+
* scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the
|
|
367
|
+
* offending value.
|
|
330
368
|
*
|
|
331
369
|
* Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this
|
|
332
370
|
* module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next
|
|
333
|
-
* one that declares a search.
|
|
334
|
-
*
|
|
335
|
-
*
|
|
371
|
+
* one that declares a search. Only GitHub and GHE declare one today, so the character class below is GitHub's in
|
|
372
|
+
* practice; it is stated as the common part of any query language because each class breaks a query on its own
|
|
373
|
+
* terms — a quote closes its own qualifier, a control character cannot appear at all, whitespace delimits the
|
|
374
|
+
* next qualifier — but a provider whose names legitimately carry one (Azure DevOps project names can contain
|
|
375
|
+
* spaces) needs its own rule alongside its `supported*Search` capability rather than an exception here.
|
|
376
|
+
*
|
|
377
|
+
* EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own
|
|
378
|
+
* sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the
|
|
379
|
+
* provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND
|
|
380
|
+
* control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so
|
|
381
|
+
* `'gitkraken\n'` and `'gitkraken\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this
|
|
382
|
+
* is wider than `String.trim()`, which leaves control characters in place.
|
|
336
383
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
384
|
+
* None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still
|
|
385
|
+
* empties, an INNER space or control character still splits (`'git\u0000kraken'` emits `git kraken`, two
|
|
386
|
+
* tokens), and a quote still alters wherever it sits.
|
|
339
387
|
*/
|
|
340
388
|
function isUsableSearchScopeName(name: string): boolean {
|
|
389
|
+
const stripped = stripSearchScopeEdges(name);
|
|
341
390
|
// eslint-disable-next-line no-control-regex
|
|
342
|
-
return
|
|
391
|
+
return stripped.length > 0 && !/["\u0000-\u001f\u007f\s]/.test(stripped);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */
|
|
395
|
+
function stripSearchScopeEdges(value: string): string {
|
|
396
|
+
// eslint-disable-next-line no-control-regex
|
|
397
|
+
return value.replace(/^[\s\u0000-\u001f\u007f]+|[\s\u0000-\u001f\u007f]+$/g, '');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is
|
|
402
|
+
* usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.
|
|
403
|
+
*/
|
|
404
|
+
function unusableSearchScopeNames(org: string | undefined, repos: readonly ProviderRepoInput[] | undefined): string[] {
|
|
405
|
+
const unusable: string[] = [];
|
|
406
|
+
|
|
407
|
+
// An EMPTY org is "no org supplied" and falls through to the remaining scopes; any other unusable value WAS
|
|
408
|
+
// supplied, so it is refused rather than dropped.
|
|
409
|
+
if (org != null && org.length > 0 && !isUsableSearchScopeName(org)) {
|
|
410
|
+
unusable.push(org);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule
|
|
414
|
+
// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the
|
|
415
|
+
// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches
|
|
416
|
+
// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —
|
|
417
|
+
// and reporting it as `undefined/a` would name a value the caller never passed.
|
|
418
|
+
for (const repo of repos ?? []) {
|
|
419
|
+
const namespace = repo.namespace ?? '';
|
|
420
|
+
const name = repo.name ?? '';
|
|
421
|
+
const path = `${namespace}/${name}`;
|
|
422
|
+
// BOTH the composite and each half, because neither sees what the other does:
|
|
423
|
+
// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character
|
|
424
|
+
// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.
|
|
425
|
+
// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where
|
|
426
|
+
// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.
|
|
427
|
+
// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.
|
|
428
|
+
if (
|
|
429
|
+
!isUsableSearchScopeName(path) ||
|
|
430
|
+
stripSearchScopeEdges(namespace).length === 0 ||
|
|
431
|
+
stripSearchScopeEdges(name).length === 0
|
|
432
|
+
) {
|
|
433
|
+
unusable.push(path);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return unusable;
|
|
343
438
|
}
|
|
344
439
|
|
|
345
440
|
/**
|
|
@@ -351,32 +446,39 @@ function isUsableSearchScopeName(name: string): boolean {
|
|
|
351
446
|
* warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is
|
|
352
447
|
* the warning layer's business.
|
|
353
448
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
449
|
+
* `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so
|
|
450
|
+
* their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied
|
|
451
|
+
* but cannot be used is never reported as a missing one.
|
|
356
452
|
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* it (`toGitHubIssueSearchScopeQualifiers` emits no bare `org:`, which GitHub would reject), so an org of `' '`
|
|
360
|
-
* or `'"'` would pass a length check, emit NO scope qualifier at all, and leave a search of the entire host —
|
|
361
|
-
* measured at 52 million issues across unrelated accounts. Whitespace and quotes are exactly what a name pasted
|
|
362
|
-
* from a config or a URL degrades to, so this is reachable without anything adversarial.
|
|
453
|
+
* Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being
|
|
454
|
+
* non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.
|
|
363
455
|
*/
|
|
364
456
|
export function resolveIssueSearchScope(
|
|
365
457
|
repos: ProviderReposInput | undefined,
|
|
366
458
|
org: string | undefined,
|
|
367
459
|
criteria: IssueSearchCriteria | undefined,
|
|
368
460
|
): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {
|
|
461
|
+
let resolvedRepos: ProviderRepoInput[] | undefined;
|
|
462
|
+
|
|
369
463
|
if (repos?.length) {
|
|
370
464
|
// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.
|
|
371
|
-
if (repos.some(r => typeof r === 'string' || typeof r === 'number'))
|
|
465
|
+
if (repos.some(r => typeof r === 'string' || typeof r === 'number')) {
|
|
466
|
+
return { rejection: { reason: 'repo-ids' } };
|
|
467
|
+
}
|
|
372
468
|
|
|
373
|
-
|
|
469
|
+
resolvedRepos = repos as ProviderRepoInput[];
|
|
374
470
|
}
|
|
375
471
|
|
|
376
|
-
|
|
472
|
+
// Checked BEFORE the "is it scoped at all" rule below, so an unusable value is never reported as a missing
|
|
473
|
+
// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.
|
|
474
|
+
const unusable = unusableSearchScopeNames(org, resolvedRepos);
|
|
475
|
+
if (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };
|
|
476
|
+
|
|
477
|
+
if (resolvedRepos != null) return { repos: resolvedRepos };
|
|
478
|
+
if (org != null && org.length > 0) return {};
|
|
377
479
|
if (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};
|
|
378
480
|
|
|
379
|
-
return { rejection: 'unscoped' };
|
|
481
|
+
return { rejection: { reason: 'unscoped' } };
|
|
380
482
|
}
|
|
381
483
|
|
|
382
484
|
/**
|
|
@@ -2,7 +2,7 @@ import type { CollectionMetadata } from '@gitkraken/provider-apis';
|
|
|
2
2
|
import type { IssueShape, IssueSorting } from '../../../git/models/issue.js';
|
|
3
3
|
import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
|
|
4
4
|
import { mapBounded } from '../../../utils/promise.js';
|
|
5
|
-
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
5
|
+
import { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';
|
|
6
6
|
import type { IntegrationIds } from '../constants.js';
|
|
7
7
|
import { providerFanOutConcurrency } from '../constants.js';
|
|
8
8
|
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
@@ -452,10 +452,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
452
452
|
// Partial project discovery means some projects' issues are missing from this page; propagate it so the
|
|
453
453
|
// page reports fetchFailed even when every discovered project's own read succeeded.
|
|
454
454
|
let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
|
|
455
|
-
|
|
456
|
-
// `truncated`; surface it as `page.truncated` so a windowed read isn't published as having drained each
|
|
457
|
-
// project completely.
|
|
458
|
-
let projectTruncated = projectDiscoveryTruncated;
|
|
455
|
+
let truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;
|
|
459
456
|
let drainMetadata: CollectionMetadata | undefined;
|
|
460
457
|
for (const { project, value: result, warning } of perProject) {
|
|
461
458
|
const key = projectKey(project);
|
|
@@ -465,7 +462,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
465
462
|
// single failing token repeats verbatim once per project in the window.
|
|
466
463
|
appendDedupedWarning(warnings, warning);
|
|
467
464
|
fetchFailed = true;
|
|
468
|
-
|
|
465
|
+
truncationRecovery = 'none';
|
|
469
466
|
}
|
|
470
467
|
// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;
|
|
471
468
|
// mark the aggregate as fetchFailed so an empty result isn't mistaken for "no issues".
|
|
@@ -475,7 +472,8 @@ export async function listIssueTrackerIssuesPage(
|
|
|
475
472
|
if (result != null) {
|
|
476
473
|
items.push(...result.values);
|
|
477
474
|
if (result.truncated) {
|
|
478
|
-
|
|
475
|
+
const recovery = result.recovery;
|
|
476
|
+
truncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';
|
|
479
477
|
}
|
|
480
478
|
if (result.metadata != null) {
|
|
481
479
|
drainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);
|
|
@@ -497,29 +495,33 @@ export async function listIssueTrackerIssuesPage(
|
|
|
497
495
|
// publishes. A no-op when nothing merged, since the tracker already ordered that single run.
|
|
498
496
|
const orderedItems = ordering.order(items);
|
|
499
497
|
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
498
|
+
const projectReadReportedTruncation = truncationRecovery != null;
|
|
499
|
+
const drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);
|
|
500
|
+
for (const warning of drainAssessment.warnings) {
|
|
501
|
+
// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with
|
|
502
|
+
// the structured warning below, while retaining specific failures and omissions.
|
|
503
|
+
if (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;
|
|
504
|
+
|
|
505
|
+
appendDedupedWarning(warnings, warning);
|
|
506
|
+
}
|
|
507
507
|
fetchFailed = fetchFailed || drainAssessment.fetchFailed;
|
|
508
|
-
|
|
508
|
+
if (drainAssessment.truncated) {
|
|
509
|
+
truncationRecovery = 'none';
|
|
510
|
+
}
|
|
509
511
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
512
|
+
const projectTruncated = truncationRecovery != null;
|
|
513
|
+
const scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';
|
|
514
|
+
if (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {
|
|
515
|
+
appendDedupedWarning(
|
|
516
|
+
warnings,
|
|
515
517
|
incompleteReadWarning(
|
|
516
518
|
options.providerId,
|
|
517
519
|
domain,
|
|
518
520
|
options.connectionId,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
scopeTooLarge
|
|
522
|
+
? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'
|
|
523
|
+
: 'Some issues were omitted; the provider returned an incomplete result.',
|
|
524
|
+
scopeTooLarge ? 'scope-too-large' : 'exhausted',
|
|
523
525
|
),
|
|
524
526
|
);
|
|
525
527
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
otherWarning,
|
|
26
26
|
truncationWarning,
|
|
27
27
|
unsupportedIssueSearchCriteriaWarning,
|
|
28
|
+
unusableSearchScopeMessage,
|
|
28
29
|
} from './warnings.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -69,11 +70,7 @@ export async function searchIssuesPage(
|
|
|
69
70
|
criteria?: IssueSearchCriteria;
|
|
70
71
|
page?: number;
|
|
71
72
|
cursor?: string;
|
|
72
|
-
/**
|
|
73
|
-
* Page size PER RELATIONSHIP: each one is its own provider query, so a page of an N-relationship search
|
|
74
|
-
* returns up to `N × itemsPerPage` items before the url dedupe, and fewer where they overlap.
|
|
75
|
-
* `page.itemsPerPage` reports what actually came back.
|
|
76
|
-
*/
|
|
73
|
+
/** Per relationship, not per page; see {@link IntegrationManager.searchIssuesPage}. */
|
|
77
74
|
itemsPerPage?: number;
|
|
78
75
|
forceSync?: boolean;
|
|
79
76
|
connectionId?: string;
|
|
@@ -117,8 +114,18 @@ export async function searchIssuesPage(
|
|
|
117
114
|
return refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));
|
|
118
115
|
}
|
|
119
116
|
|
|
117
|
+
const resolved = resolveIssueSearchCriteria(options.providerId, options.criteria);
|
|
118
|
+
if (resolved.rejection != null) {
|
|
119
|
+
return refused(
|
|
120
|
+
unsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, resolved.rejection),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// AFTER the criteria check, which is what reports a provider with no filtered issue search at all: that is
|
|
125
|
+
// the more fundamental refusal, and answering "your scope name is malformed" to a caller whose provider has
|
|
126
|
+
// no such search names the wrong defect. Matches the pull-request twin, which probes existence first.
|
|
120
127
|
const scope = resolveIssueSearchScope(options.repos, options.org, options.criteria);
|
|
121
|
-
switch (scope.rejection) {
|
|
128
|
+
switch (scope.rejection?.reason) {
|
|
122
129
|
case 'unscoped':
|
|
123
130
|
return refused(
|
|
124
131
|
otherWarning(
|
|
@@ -140,13 +147,16 @@ export async function searchIssuesPage(
|
|
|
140
147
|
'A filtered issue search cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.',
|
|
141
148
|
),
|
|
142
149
|
);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.
|
|
151
|
+
case 'unusable-scope':
|
|
152
|
+
return refused(
|
|
153
|
+
otherWarning(
|
|
154
|
+
options.providerId,
|
|
155
|
+
domain,
|
|
156
|
+
options.connectionId,
|
|
157
|
+
unusableSearchScopeMessage('issue search scopes', scope.rejection.scopes),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
const readPage = (cursor: string | undefined) =>
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
pullRequestSearchCapResultWarning,
|
|
25
25
|
truncationWarning,
|
|
26
26
|
unsupportedPullRequestSearchCriteriaWarning,
|
|
27
|
+
unusableSearchScopeMessage,
|
|
27
28
|
} from './warnings.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -44,6 +45,7 @@ export async function searchPullRequestsPage(
|
|
|
44
45
|
criteria?: PullRequestSearchCriteria;
|
|
45
46
|
page?: number;
|
|
46
47
|
cursor?: string;
|
|
48
|
+
/** Per relationship × state facet, not per page; see {@link IntegrationManager.searchPullRequestsPage}. */
|
|
47
49
|
itemsPerPage?: number;
|
|
48
50
|
forceSync?: boolean;
|
|
49
51
|
connectionId?: string;
|
|
@@ -100,7 +102,7 @@ export async function searchPullRequestsPage(
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
const scope = resolvePullRequestSearchScope(options.providerId, options.repos, options.org, options.criteria);
|
|
103
|
-
switch (scope.rejection) {
|
|
105
|
+
switch (scope.rejection?.reason) {
|
|
104
106
|
case 'unscoped':
|
|
105
107
|
return refused(
|
|
106
108
|
otherWarning(
|
|
@@ -125,10 +127,22 @@ export async function searchPullRequestsPage(
|
|
|
125
127
|
unsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {
|
|
126
128
|
reason: 'unsupported-criteria',
|
|
127
129
|
criteria: [
|
|
128
|
-
scope.rejection === 'unsupported-repository-scope'
|
|
130
|
+
scope.rejection.reason === 'unsupported-repository-scope'
|
|
131
|
+
? 'repositoryScope'
|
|
132
|
+
: 'organizationScope',
|
|
129
133
|
],
|
|
130
134
|
}),
|
|
131
135
|
);
|
|
136
|
+
// A scope the query cannot spell, refused rather than sanitized — see `isUsableSearchScopeName`.
|
|
137
|
+
case 'unusable-scope':
|
|
138
|
+
return refused(
|
|
139
|
+
otherWarning(
|
|
140
|
+
options.providerId,
|
|
141
|
+
domain,
|
|
142
|
+
options.connectionId,
|
|
143
|
+
unusableSearchScopeMessage('pull request search scopes', scope.rejection.scopes),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
const readPage = (cursor: string | undefined) =>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { IssueShape } from '../../../git/models/issue.js';
|
|
2
|
+
import type { IntegrationIds } from '../constants.js';
|
|
3
|
+
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
4
|
+
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
5
|
+
import type { ProviderResult, ProviderWarning } from '../results.js';
|
|
6
|
+
import { isIssuesHostIntegrationId } from '../utils/integration.utils.js';
|
|
7
|
+
import type { ProviderReadContext } from './context.js';
|
|
8
|
+
import { runCaptured } from './drains.js';
|
|
9
|
+
import { issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
|
|
10
|
+
|
|
11
|
+
export interface TrackerIssueResult {
|
|
12
|
+
key: string;
|
|
13
|
+
/**
|
|
14
|
+
* The resolved issue, or `undefined` when it provably does not exist or is not visible to this connection.
|
|
15
|
+
* A failed read returns no item and sets `fetchFailed`.
|
|
16
|
+
*/
|
|
17
|
+
issue?: IssueShape;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Issue trackers are cloud-only, so this read takes no `domain`. */
|
|
21
|
+
export async function getTrackerIssue(
|
|
22
|
+
ctx: ProviderReadContext,
|
|
23
|
+
options: {
|
|
24
|
+
providerId: IntegrationIds;
|
|
25
|
+
resourceId: string;
|
|
26
|
+
resourceUrl?: string;
|
|
27
|
+
key: string;
|
|
28
|
+
connectionId?: string;
|
|
29
|
+
},
|
|
30
|
+
): Promise<ProviderResult<TrackerIssueResult>> {
|
|
31
|
+
const refused = (warning: ProviderWarning): ProviderResult<TrackerIssueResult> => ({
|
|
32
|
+
items: [],
|
|
33
|
+
warnings: [warning],
|
|
34
|
+
fetchFailed: true,
|
|
35
|
+
});
|
|
36
|
+
const surface = 'Issue resolution by key';
|
|
37
|
+
|
|
38
|
+
if (!isIssuesHostIntegrationId(options.providerId)) {
|
|
39
|
+
return refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.resourceId.trim().length === 0) {
|
|
43
|
+
return refused(
|
|
44
|
+
otherWarning(options.providerId, undefined, options.connectionId, `${surface} requires a resource id.`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.key.trim().length === 0) {
|
|
49
|
+
return refused(
|
|
50
|
+
otherWarning(options.providerId, undefined, options.connectionId, `${surface} requires an issue key.`),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resourceUrl = options.resourceUrl?.trim() || undefined;
|
|
55
|
+
if (options.providerId === IssuesCloudHostIntegrationId.Jira && resourceUrl == null) {
|
|
56
|
+
return refused(
|
|
57
|
+
otherWarning(
|
|
58
|
+
options.providerId,
|
|
59
|
+
undefined,
|
|
60
|
+
options.connectionId,
|
|
61
|
+
`${surface} requires the Jira resource URL so the result contains a browser link without resource discovery.`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId);
|
|
67
|
+
if (integration == null) {
|
|
68
|
+
const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId);
|
|
69
|
+
return { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };
|
|
70
|
+
}
|
|
71
|
+
if (!isIssuesIntegration(integration)) {
|
|
72
|
+
return refused(issueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, surface));
|
|
73
|
+
}
|
|
74
|
+
if (!integration.supportsIssueLookupByResourceId) {
|
|
75
|
+
return refused(
|
|
76
|
+
otherWarning(
|
|
77
|
+
options.providerId,
|
|
78
|
+
undefined,
|
|
79
|
+
options.connectionId,
|
|
80
|
+
`${surface} is not supported by '${options.providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const domain = ctx.domainForRead(integration, options.providerId, options.connectionId);
|
|
86
|
+
const issue = await runCaptured(
|
|
87
|
+
options.providerId,
|
|
88
|
+
domain,
|
|
89
|
+
options.connectionId,
|
|
90
|
+
() =>
|
|
91
|
+
integration.getIssueByResourceIdResult(options.resourceId, options.key, {
|
|
92
|
+
connectionId: options.connectionId,
|
|
93
|
+
resourceUrl: resourceUrl,
|
|
94
|
+
}),
|
|
95
|
+
{ warnOnMissingSession: true },
|
|
96
|
+
);
|
|
97
|
+
if (issue.warning != null) {
|
|
98
|
+
return refused(issue.warning);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
items: [{ key: options.key, ...(issue.value != null ? { issue: issue.value } : {}) }],
|
|
103
|
+
warnings: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
PullRequestSorting,
|
|
9
9
|
} from '../providerFilters.js';
|
|
10
10
|
import { providersMetadata } from '../providers/models.js';
|
|
11
|
-
import type { ProviderWarning } from '../results.js';
|
|
11
|
+
import type { ProviderWarning, ProviderWarningOmissionRecovery } from '../results.js';
|
|
12
12
|
import type { IssueSearchCriteriaRejection, PullRequestSearchCriteriaRejection } from './filters.js';
|
|
13
13
|
import type { UnmergeableIssueSort, UnsupportedIssueSortRejection } from './ordering.js';
|
|
14
14
|
|
|
@@ -101,21 +101,23 @@ export function issueTrackerOnlySurfaceWarning(
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
/** Why a facade-driven read returned fewer results than requested. */
|
|
105
|
+
export type IncompleteReadCause = 'interrupted' | 'page-budget' | 'exhausted' | 'scope-too-large';
|
|
106
|
+
|
|
107
|
+
function recoveryForIncompleteReadCause(cause: IncompleteReadCause): ProviderWarningOmissionRecovery {
|
|
108
|
+
switch (cause) {
|
|
109
|
+
case 'page-budget':
|
|
110
|
+
return 'page-budget';
|
|
111
|
+
case 'scope-too-large':
|
|
112
|
+
return 'narrow-scope';
|
|
113
|
+
case 'interrupted':
|
|
114
|
+
case 'exhausted':
|
|
115
|
+
return 'none';
|
|
116
|
+
default:
|
|
117
|
+
cause satisfies never;
|
|
118
|
+
return 'none';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
123
|
* Builds the warning for a read that returned less than everything, carrying the two facts a consumer cannot
|
|
@@ -143,7 +145,7 @@ export function incompleteReadWarning(
|
|
|
143
145
|
...warning,
|
|
144
146
|
omission: {
|
|
145
147
|
kind: 'pagination-incomplete',
|
|
146
|
-
recovery: cause
|
|
148
|
+
recovery: recoveryForIncompleteReadCause(cause),
|
|
147
149
|
},
|
|
148
150
|
};
|
|
149
151
|
}
|
|
@@ -192,6 +194,10 @@ function truncationMessage(id: IntegrationIds, readKind: TruncatedReadKind, caus
|
|
|
192
194
|
return `${readKind} read for '${id}' stopped at its page budget; more results can be read by raising it.`;
|
|
193
195
|
case 'exhausted':
|
|
194
196
|
return `${readKind} read for '${id}' was truncated and cannot be continued; results may be incomplete.`;
|
|
197
|
+
case 'scope-too-large':
|
|
198
|
+
// Says what is true — the scope is larger than a read of it — rather than blaming the provider for a
|
|
199
|
+
// ceiling that is ours, and names the remedy that exists, since no retry or budget has one.
|
|
200
|
+
return `${readKind} read for '${id}' covers more than one read can return; narrow the scope to read the rest.`;
|
|
195
201
|
}
|
|
196
202
|
// No `default`: `IncompleteReadCause` is declared in this file, so `noImplicitReturns` already fails the
|
|
197
203
|
// build here if a cause is added without its own wording. (`collectionOmissionMessage`'s `satisfies never`
|
|
@@ -255,6 +261,25 @@ export function unsupportedFiltersWarning(
|
|
|
255
261
|
);
|
|
256
262
|
}
|
|
257
263
|
|
|
264
|
+
/**
|
|
265
|
+
* The one sentence every `unusable-scope` refusal uses — the five surfaces that validate a search scope (both
|
|
266
|
+
* searches, both count probes, and issue broadening) differ only in the noun and in whether they name a count
|
|
267
|
+
* key, so the reason a scope is unusable is written once. `prefix` is what the count probes put in front of it
|
|
268
|
+
* to name their key.
|
|
269
|
+
*
|
|
270
|
+
* NAMES the offending values, because which failure mode applied is not deducible from the value: a quote makes
|
|
271
|
+
* `git"kraken` a request for the real and different org `gitkraken`, a space makes `my org` a search of `my`
|
|
272
|
+
* filtered by the text `org`, and a value of only quotes or whitespace leaves no scope at all. The caller is the
|
|
273
|
+
* only layer that knows which scope it meant.
|
|
274
|
+
*
|
|
275
|
+
* Says how to fix it rather than what was stripped: a consumer cannot mirror this rule (unlike free-text
|
|
276
|
+
* sanitizing, whose rules are published for exactly that), so "pass the scope as the provider names it" is the
|
|
277
|
+
* actionable half.
|
|
278
|
+
*/
|
|
279
|
+
export function unusableSearchScopeMessage(noun: string, scopes: readonly string[], prefix?: string): string {
|
|
280
|
+
return `${prefix != null ? `${prefix}: ` : ''}The requested ${noun} (${scopes.map(s => JSON.stringify(s)).join(', ')}) cannot be used as given: a scope name cannot contain quotes, or an inner space or control character, which a query would drop or read as a second qualifier — searching a different scope, or none. Pass the scope exactly as the provider names it.`;
|
|
281
|
+
}
|
|
282
|
+
|
|
258
283
|
/** Warning for a filtered pull-request search the provider cannot run as requested. */
|
|
259
284
|
export function unsupportedPullRequestSearchCriteriaWarning(
|
|
260
285
|
id: IntegrationIds,
|