@gitkraken/core-gitlens 0.5.113 → 0.5.114
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 +17 -1
- package/dist/git/cache.d.ts +4 -0
- package/dist/git/cache.d.ts.map +1 -1
- package/dist/git/cache.js +33 -0
- package/dist/git/cache.js.map +1 -1
- package/dist/git/context.d.ts +10 -0
- package/dist/git/context.d.ts.map +1 -1
- package/dist/git/context.js.map +1 -1
- package/dist/git/errors.d.ts +6 -3
- package/dist/git/errors.d.ts.map +1 -1
- package/dist/git/errors.js +9 -1
- package/dist/git/errors.js.map +1 -1
- package/dist/git/features.d.ts +1 -1
- package/dist/git/features.d.ts.map +1 -1
- package/dist/git/features.js +38 -0
- package/dist/git/features.js.map +1 -1
- package/dist/git/gitHealth.d.ts +153 -0
- package/dist/git/gitHealth.d.ts.map +1 -0
- package/dist/git/gitHealth.js +353 -0
- package/dist/git/gitHealth.js.map +1 -0
- package/dist/git/models/search.d.ts +6 -3
- package/dist/git/models/search.d.ts.map +1 -1
- package/dist/git/models/search.js +5 -1
- package/dist/git/models/search.js.map +1 -1
- package/dist/git/parsers/diffParser.d.ts +8 -2
- package/dist/git/parsers/diffParser.d.ts.map +1 -1
- package/dist/git/parsers/diffParser.js +27 -4
- package/dist/git/parsers/diffParser.js.map +1 -1
- package/dist/git/providers/config.d.ts +36 -3
- package/dist/git/providers/config.d.ts.map +1 -1
- package/dist/git/providers/graph.d.ts +9 -0
- package/dist/git/providers/graph.d.ts.map +1 -1
- package/dist/git/providers/maintenance.d.ts +222 -0
- package/dist/git/providers/maintenance.d.ts.map +1 -0
- package/dist/git/providers/maintenance.js +20 -0
- package/dist/git/providers/maintenance.js.map +1 -0
- package/dist/git/providers/operations.d.ts +10 -0
- package/dist/git/providers/operations.d.ts.map +1 -1
- package/dist/git/providers/pausedOperations.d.ts +3 -0
- package/dist/git/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git/providers/provider.d.ts +2 -0
- package/dist/git/providers/provider.d.ts.map +1 -1
- package/dist/git/providers/tags.d.ts +3 -0
- package/dist/git/providers/tags.d.ts.map +1 -1
- package/dist/git/repositoryService.d.ts +1 -0
- package/dist/git/repositoryService.d.ts.map +1 -1
- package/dist/git/repositoryService.js.map +1 -1
- package/dist/git/run.types.d.ts +8 -0
- package/dist/git/run.types.d.ts.map +1 -1
- package/dist/git/service.js +3 -0
- package/dist/git/service.js.map +1 -1
- package/dist/git/utils/search.utils.d.ts.map +1 -1
- package/dist/git/utils/search.utils.js +112 -5
- package/dist/git/utils/search.utils.js.map +1 -1
- package/dist/git/watching/watchService.d.ts +3 -0
- package/dist/git/watching/watchService.d.ts.map +1 -1
- package/dist/git/watching/watchService.js +8 -0
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git-cli/cliGitProvider.d.ts +11 -0
- package/dist/git-cli/cliGitProvider.d.ts.map +1 -1
- package/dist/git-cli/cliGitProvider.js +12 -0
- package/dist/git-cli/cliGitProvider.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +23 -1
- package/dist/git-cli/exec/git.d.ts.map +1 -1
- package/dist/git-cli/exec/git.js +83 -8
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/exec/gitQueue.d.ts +2 -0
- package/dist/git-cli/exec/gitQueue.d.ts.map +1 -1
- package/dist/git-cli/exec/gitQueue.js +19 -17
- package/dist/git-cli/exec/gitQueue.js.map +1 -1
- package/dist/git-cli/parsers/logParser.d.ts.map +1 -1
- package/dist/git-cli/parsers/logParser.js +4 -1
- package/dist/git-cli/parsers/logParser.js.map +1 -1
- package/dist/git-cli/providers/blame.d.ts.map +1 -1
- package/dist/git-cli/providers/blame.js +4 -7
- package/dist/git-cli/providers/blame.js.map +1 -1
- package/dist/git-cli/providers/branches.d.ts.map +1 -1
- package/dist/git-cli/providers/branches.js +13 -3
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.d.ts.map +1 -1
- package/dist/git-cli/providers/commits.js +34 -3
- package/dist/git-cli/providers/commits.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +27 -0
- package/dist/git-cli/providers/config.d.ts.map +1 -1
- package/dist/git-cli/providers/config.js +31 -6
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/graph.d.ts +8 -0
- package/dist/git-cli/providers/graph.d.ts.map +1 -1
- package/dist/git-cli/providers/graph.js +85 -6
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/maintenance.d.ts +306 -0
- package/dist/git-cli/providers/maintenance.d.ts.map +1 -0
- package/dist/git-cli/providers/maintenance.js +2129 -0
- package/dist/git-cli/providers/maintenance.js.map +1 -0
- package/dist/git-cli/providers/operations.d.ts +1 -0
- package/dist/git-cli/providers/operations.d.ts.map +1 -1
- package/dist/git-cli/providers/operations.js +8 -0
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
- package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git-cli/providers/pausedOperations.js +1 -1
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/tags.d.ts +3 -0
- package/dist/git-cli/providers/tags.d.ts.map +1 -1
- package/dist/git-cli/providers/tags.js +23 -1
- package/dist/git-cli/providers/tags.js.map +1 -1
- package/dist/plus/agents/agentCapabilities.d.ts +77 -0
- package/dist/plus/agents/agentCapabilities.d.ts.map +1 -0
- package/dist/plus/agents/agentCapabilities.js +197 -0
- package/dist/plus/agents/agentCapabilities.js.map +1 -0
- package/dist/plus/agents/providers/claudeCodeTranscript.d.ts +77 -13
- package/dist/plus/agents/providers/claudeCodeTranscript.d.ts.map +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +263 -55
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/agents/providers/{claudeCodeProvider.d.ts → gkAgentProvider.d.ts} +52 -19
- package/dist/plus/agents/providers/gkAgentProvider.d.ts.map +1 -0
- package/dist/plus/agents/providers/{claudeCodeProvider.js → gkAgentProvider.js} +935 -262
- package/dist/plus/agents/providers/gkAgentProvider.js.map +1 -0
- package/dist/plus/agents/types.d.ts +113 -35
- package/dist/plus/agents/types.d.ts.map +1 -1
- package/dist/plus/agents/types.js +7 -4
- package/dist/plus/agents/types.js.map +1 -1
- package/dist/plus/ai/models/promptTemplates.d.ts +1 -1
- package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
- package/dist/plus/ai/prompts.d.ts +4 -0
- package/dist/plus/ai/prompts.d.ts.map +1 -1
- package/dist/plus/ai/prompts.js +59 -4
- package/dist/plus/ai/prompts.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +44 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +318 -25
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/issueSearchQuery.d.ts +13 -0
- package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
- package/dist/plus/git-github/api/issueSearchQuery.js +41 -0
- package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
- package/dist/plus/git-github/providers/github/blame.d.ts.map +1 -1
- package/dist/plus/git-github/providers/github/blame.js +2 -1
- package/dist/plus/git-github/providers/github/blame.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +11 -0
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +4 -0
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +26 -0
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -0
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +31 -0
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +1 -1
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +9 -0
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +7 -0
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts +47 -10
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +144 -33
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +18 -0
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +37 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +51 -0
- package/dist/plus/integrations/reads/issueBatch.d.ts.map +1 -0
- package/dist/plus/integrations/reads/issueBatch.js +91 -0
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -0
- package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +5 -3
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/utils/promiseCache.d.ts +19 -0
- package/dist/utils/promiseCache.d.ts.map +1 -1
- package/dist/utils/promiseCache.js +30 -0
- package/dist/utils/promiseCache.js.map +1 -1
- package/dist/utils/resourceUsage.d.ts +5 -0
- package/dist/utils/resourceUsage.d.ts.map +1 -0
- package/dist/utils/resourceUsage.js +2 -0
- package/dist/utils/resourceUsage.js.map +1 -0
- package/dist/utils/string.d.ts +12 -0
- package/dist/utils/string.d.ts.map +1 -1
- package/dist/utils/string.js +36 -1
- package/dist/utils/string.js.map +1 -1
- package/docs/integrations.md +29 -25
- package/docs/kepler-read-api-parity.md +12 -4
- package/package.json +11 -3
- package/src/git/cache.ts +36 -0
- package/src/git/context.ts +11 -0
- package/src/git/errors.ts +13 -1
- package/src/git/features.ts +51 -0
- package/src/git/gitHealth.ts +522 -0
- package/src/git/models/search.ts +19 -4
- package/src/git/parsers/diffParser.ts +27 -4
- package/src/git/providers/config.ts +42 -1
- package/src/git/providers/graph.ts +12 -0
- package/src/git/providers/maintenance.ts +224 -0
- package/src/git/providers/operations.ts +10 -0
- package/src/git/providers/pausedOperations.ts +3 -0
- package/src/git/providers/provider.ts +2 -0
- package/src/git/providers/tags.ts +1 -0
- package/src/git/repositoryService.ts +1 -0
- package/src/git/run.types.ts +8 -0
- package/src/git/service.ts +3 -0
- package/src/git/utils/search.utils.ts +129 -5
- package/src/git/watching/watchService.ts +10 -0
- package/src/git-cli/cliGitProvider.ts +25 -0
- package/src/git-cli/exec/git.ts +120 -6
- package/src/git-cli/exec/gitQueue.ts +19 -17
- package/src/git-cli/parsers/logParser.ts +4 -1
- package/src/git-cli/providers/blame.ts +3 -8
- package/src/git-cli/providers/branches.ts +28 -8
- package/src/git-cli/providers/commits.ts +34 -2
- package/src/git-cli/providers/config.ts +38 -6
- package/src/git-cli/providers/graph.ts +107 -7
- package/src/git-cli/providers/maintenance.ts +2599 -0
- package/src/git-cli/providers/operations.ts +11 -0
- package/src/git-cli/providers/pausedOperations.ts +7 -2
- package/src/git-cli/providers/tags.ts +36 -1
- package/src/plus/agents/agentCapabilities.ts +286 -0
- package/src/plus/agents/providers/claudeCodeTranscript.ts +313 -58
- package/src/plus/agents/providers/{claudeCodeProvider.ts → gkAgentProvider.ts} +1123 -277
- package/src/plus/agents/types.ts +137 -40
- package/src/plus/ai/models/promptTemplates.ts +1 -1
- package/src/plus/ai/prompts.ts +62 -4
- package/src/plus/git-github/api/github.ts +353 -24
- package/src/plus/git-github/api/issueSearchQuery.ts +49 -1
- package/src/plus/git-github/providers/github/blame.ts +2 -1
- package/src/plus/integrations/integrationService.ts +15 -0
- package/src/plus/integrations/manager.ts +26 -0
- package/src/plus/integrations/models/gitHostIntegration.ts +48 -0
- package/src/plus/integrations/models/integration.ts +1 -0
- package/src/plus/integrations/providers/github.ts +18 -0
- package/src/plus/integrations/reads/broaden.ts +282 -70
- package/src/plus/integrations/reads/filters.ts +39 -1
- package/src/plus/integrations/reads/issueBatch.ts +197 -0
- package/src/plus/integrations/reads/sweeps.ts +19 -12
- package/src/utils/promiseCache.ts +43 -0
- package/src/utils/resourceUsage.ts +5 -0
- package/src/utils/string.ts +38 -1
- package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +0 -1
- package/dist/plus/agents/providers/claudeCodeProvider.js.map +0 -1
|
@@ -1789,6 +1789,54 @@ export abstract class GitHostIntegration<
|
|
|
1789
1789
|
cancellation?: AbortSignal,
|
|
1790
1790
|
): Promise<(number | undefined)[] | undefined>;
|
|
1791
1791
|
|
|
1792
|
+
/**
|
|
1793
|
+
* Result-returning wrapper for the BATCH issue read: resolves several `(owner, repo, number)` coordinates in
|
|
1794
|
+
* one request. Recovers thrown errors into `{ error }` like the reads around it.
|
|
1795
|
+
*
|
|
1796
|
+
* Distinct from every search on this class, and deliberately so. A search answers "what matches"; this answers
|
|
1797
|
+
* "does this exact issue exist", which is the question a caller correlating a branch name to an issue is
|
|
1798
|
+
* actually asking. It has no result ceiling, no ordering and no cursor, and — the property that matters most —
|
|
1799
|
+
* an absent slot is a PROVEN ABSENCE rather than "not found within a page budget", so a caller can cache a
|
|
1800
|
+
* miss instead of re-walking for it forever.
|
|
1801
|
+
*
|
|
1802
|
+
* Results come back POSITIONALLY — one per input coordinate, in order — for the same reason
|
|
1803
|
+
* {@link countIssuesResult} does: a caller's key must never reach the provider query. `undefined` in a slot
|
|
1804
|
+
* means the issue does not exist or is not visible to this token, never that the read failed.
|
|
1805
|
+
*/
|
|
1806
|
+
async getIssuesBatchResult(
|
|
1807
|
+
coordinates: readonly { owner: string; repo: string; number: number }[],
|
|
1808
|
+
cancellation?: AbortSignal,
|
|
1809
|
+
connectionId?: string,
|
|
1810
|
+
): Promise<IntegrationResult<(IssueShape | undefined)[] | undefined>> {
|
|
1811
|
+
const scope = getScopedLogger();
|
|
1812
|
+
// `connectionId` targets a specific account (multi-account); omitted reads the primary.
|
|
1813
|
+
const session = await this.resolveReadSession(connectionId, scope);
|
|
1814
|
+
if (session == null) return undefined;
|
|
1815
|
+
|
|
1816
|
+
const start = performance.now();
|
|
1817
|
+
try {
|
|
1818
|
+
const issues = await this.getProviderIssuesBatch?.(session, coordinates, cancellation);
|
|
1819
|
+
this.resetRequestExceptionCount('getIssuesBatch');
|
|
1820
|
+
return { value: issues, duration: performance.now() - start };
|
|
1821
|
+
} catch (ex) {
|
|
1822
|
+
this.handleProviderException('getIssuesBatch', ex, { scope: scope, connectionId: connectionId });
|
|
1823
|
+
return { error: toError(ex), duration: performance.now() - start };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* OPTIONAL: only a provider that can resolve SEVERAL issues by coordinate in one request implements this.
|
|
1829
|
+
* GitHub aliases its point read into one document; the SDK exposes only a singular `getIssue` for every other
|
|
1830
|
+
* provider, and there is no plural variant to build on. A provider that can't answer doesn't implement it and
|
|
1831
|
+
* the facade refuses the read, so a caller keeps its own per-issue path rather than being handed a batch that
|
|
1832
|
+
* silently degraded into N requests.
|
|
1833
|
+
*/
|
|
1834
|
+
protected getProviderIssuesBatch?(
|
|
1835
|
+
session: ProviderAuthenticationSession,
|
|
1836
|
+
coordinates: readonly { owner: string; repo: string; number: number }[],
|
|
1837
|
+
cancellation?: AbortSignal,
|
|
1838
|
+
): Promise<(IssueShape | undefined)[] | undefined>;
|
|
1839
|
+
|
|
1792
1840
|
/** The PR twin of {@link countIssuesResult}: counts each scope's pull requests, transferring none. */
|
|
1793
1841
|
async countPullRequestsResult(
|
|
1794
1842
|
scopes: readonly { repos?: ProviderRepoInput[]; org?: string; criteria?: PullRequestSearchCriteria }[],
|
|
@@ -689,6 +689,24 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
|
|
|
689
689
|
);
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Resolves several issues by `(owner, repo, number)` in ONE request, by aliasing the point read rather than a
|
|
694
|
+
* search — see {@link GitHubApi.getIssuesBatch} for why that distinction is the whole design.
|
|
695
|
+
*/
|
|
696
|
+
protected override async getProviderIssuesBatch(
|
|
697
|
+
session: ProviderAuthenticationSession,
|
|
698
|
+
coordinates: readonly { owner: string; repo: string; number: number }[],
|
|
699
|
+
cancellation?: AbortSignal,
|
|
700
|
+
): Promise<(IssueShape | undefined)[] | undefined> {
|
|
701
|
+
return (await this.authenticationService.apis.github)?.getIssuesBatch(
|
|
702
|
+
this,
|
|
703
|
+
toTokenWithInfo(this.id, session),
|
|
704
|
+
coordinates,
|
|
705
|
+
{ baseUrl: this.apiBaseUrl, includeBody: true },
|
|
706
|
+
cancellation,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
692
710
|
/**
|
|
693
711
|
* Counts several pull-request scopes in ONE request. Like {@link countProviderIssues}, GitHub's `search`
|
|
694
712
|
* reports `issueCount` on a zero-node selection, so a count preview costs no pull-request transfer.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { IssueShape } from '../../../git/models/issue.js';
|
|
2
|
+
import { effectiveIssueSort } from '../../../git/utils/issue.utils.js';
|
|
2
3
|
import { mapBounded } from '../../../utils/promise.js';
|
|
3
4
|
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
4
5
|
import type { IntegrationIds } from '../constants.js';
|
|
5
6
|
import { providerFanOutConcurrency } from '../constants.js';
|
|
6
7
|
import type { ProviderBroadenOrg } from '../manager.js';
|
|
8
|
+
import type { GitHostIntegration } from '../models/gitHostIntegration.js';
|
|
7
9
|
import type { ProviderReposInput } from '../providers/models.js';
|
|
8
10
|
import type { ProviderBroadenResult, ProviderWarning } from '../results.js';
|
|
9
11
|
import { appendDedupedWarning } from '../results.js';
|
|
@@ -13,32 +15,78 @@ import type { ProviderReadContext } from './context.js';
|
|
|
13
15
|
import type { BroadenIssuesExhaustedOrg, BroadenIssuesOrgCursor } from './cursors.js';
|
|
14
16
|
import { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, toBroadenIssuesCursor } from './cursors.js';
|
|
15
17
|
import { drainRepositories, runCaptured } from './drains.js';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
+
import { resolveIssueSearchScope, supportsFilteredIssueSearch } from './filters.js';
|
|
19
|
+
import { resolveContinuation, toProviderPageInfo, usableCursor } from './paging.js';
|
|
20
|
+
import {
|
|
21
|
+
gitHostOnlySurfaceWarning,
|
|
22
|
+
issueSearchCapResultWarning,
|
|
23
|
+
issuesUnsupportedWarning,
|
|
24
|
+
noConnectionWarning,
|
|
25
|
+
otherWarning,
|
|
26
|
+
truncationWarning,
|
|
27
|
+
unsupportedIssueSearchCriteriaWarning,
|
|
28
|
+
} from './warnings.js';
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
* The issue-broadening fan-out:
|
|
31
|
+
* The issue-broadening fan-out: every visible issue across a set of orgs, one page at a time.
|
|
21
32
|
*
|
|
22
33
|
* What makes this its own read rather than a variant of `listIssuesPage` is that one logical page spans several
|
|
23
34
|
* provider positions — one per org — so its continuation is a per-org cursor BUNDLE (see `cursors.ts`) and its
|
|
24
35
|
* failure attribution is per provider across those orgs. Both are the reason a page number alone can't address a
|
|
25
36
|
* later page, and why {@link broadenIssues} walks prior pages itself when given only `page`.
|
|
26
37
|
*
|
|
27
|
-
*
|
|
28
|
-
* the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
38
|
+
* Each org slice runs on ONE OF TWO ENGINES, chosen by what the provider declares (#5804):
|
|
39
|
+
* - the org-scoped FILTERED SEARCH where the provider has one (GitHub/GHE): one request per page, no repositories
|
|
40
|
+
* needed at all, since the search reaches the scope by naming the org;
|
|
41
|
+
* - the REPOSITORY DRAIN plus the SDK's repo-scoped read where it doesn't (Azure DevOps, GitLab): up to 100
|
|
42
|
+
* requests to enumerate the org, and a read whose over-limit recovery walk can spend 128 more per repository
|
|
43
|
+
* and still return an incomplete set.
|
|
31
44
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
45
|
+
* Measured by running this read against the live API, three times per engine per org (median shown; each
|
|
46
|
+
* engine returned a byte-identical result set on all three, so the sets below are not sampling noise):
|
|
47
|
+
*
|
|
48
|
+
* | | repo drain + SDK read | org-scoped search |
|
|
49
|
+
* | --- | --- | --- |
|
|
50
|
+
* | BELOW the ceiling (146 issues) | 9.3s, 11 requests, 146 issues | 6.2s, 2 requests, 146 issues |
|
|
51
|
+
* | ABOVE it (1.475 issues, 217 repos) | 111s, 133 requests, 1.118 issues | 40s, 10 requests, 1.000 issues |
|
|
52
|
+
* | ...of the 1.000 most recently updated | 644 | 1.000 |
|
|
53
|
+
* | ...omissions reported | 99 × `recovery-budget`, no count | 1, carrying `totalCount: 1474` |
|
|
54
|
+
*
|
|
55
|
+
* BELOW the ceiling the two return the IDENTICAL SET (verified as set equality), so there the swap is a straight
|
|
56
|
+
* saving and nothing else. ABOVE it they return DIFFERENT SETS — overlapping on 644 items, so the divergence is
|
|
57
|
+
* not confined to the edge.
|
|
58
|
+
*
|
|
59
|
+
* Above the ceiling the search returns FEWER items and is nonetheless the more useful of the two, which is the
|
|
60
|
+
* whole reason to prefer it: its 1.000 are exactly the 1.000 most recently updated (again set equality against a
|
|
61
|
+
* ceiling-free enumeration, not a count), whereas the SDK read's 1.118 are an arbitrary subset — 474 of them from
|
|
62
|
+
* beyond that window, at the cost of omitting 356 issues that ARE in it. Its recovery walk does not close the gap
|
|
63
|
+
* it spends the budget on: all 99 of its omissions were exhausted budget.
|
|
64
|
+
*
|
|
65
|
+
* The second difference is what a consumer can SAY about the gap. At the ceiling the search succeeds and reports
|
|
66
|
+
* one quantified omission (`issueSearchCapResultWarning` carries `totalCount`, `limit` and the order the window
|
|
67
|
+
* was selected under), so a consumer can render "1.474 matched, showing the 1.000 most recently updated". The
|
|
68
|
+
* SDK read reports one `recovery-budget` omission per exhausted repository, none of which carries how many issues
|
|
69
|
+
* were missed — incompleteness that can be announced but not quantified.
|
|
70
|
+
*
|
|
71
|
+
* Note the time saving is the SMALLER half of this: 1.5× below the ceiling and 2.8× above it, against 5.5× and
|
|
72
|
+
* 13.3× fewer requests. The request count is what matters for a rate limit shared with every other read.
|
|
73
|
+
*
|
|
74
|
+
* The public surface is the SAME on both: the options, the per-org cursor bundle, the `ProviderAttribution`
|
|
75
|
+
* split, the warning dedupe and the return type. A consumer needs to know which engine ran only insofar as the
|
|
76
|
+
* warnings say so.
|
|
77
|
+
*
|
|
78
|
+
* Broadening means ALL VISIBLE, and each engine expresses that differently — `includeAllAssignees: true` on the
|
|
79
|
+
* SDK read, an OMITTED `relationships` on the search. Both resolve to no assignee constraint at all, so
|
|
80
|
+
* unassigned issues ARE included. The search's equivalent is NOT `['any-assignee']`: `assignee:*` means "has some
|
|
35
81
|
* assignee" and would silently exclude every unassigned issue, which is the opposite of broadening.
|
|
36
82
|
*
|
|
37
83
|
* Takes no `sort`, unlike `listIssuesPage` and `searchIssuesPage`. One logical page here spans several orgs, each
|
|
38
84
|
* at its own provider position in a cursor bundle, so honoring an order across them would need a k-way merge with
|
|
39
85
|
* a buffer per org rather than a sort of what arrived — the page is a slice of several independent walks, not a
|
|
40
|
-
* union of one round's results.
|
|
41
|
-
*
|
|
86
|
+
* union of one round's results. The search engine still orders each org's OWN query (it must: a ceiling policy of
|
|
87
|
+
* "the N most recent" is only correct under a guaranteed order), so a slice is ordered where the merged page is
|
|
88
|
+
* not. `searchIssuesPage({ repos, criteria: { sort } })` answers the ordered version of this question for a
|
|
89
|
+
* caller that knows its repositories.
|
|
42
90
|
*/
|
|
43
91
|
|
|
44
92
|
export interface BroadenIssuesOptions {
|
|
@@ -276,7 +324,210 @@ async function traverseToRequestedPage(
|
|
|
276
324
|
};
|
|
277
325
|
}
|
|
278
326
|
|
|
279
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* One org's issues as either engine below reports them, normalized to the single shape {@link readOrgSlice}'s
|
|
329
|
+
* bookkeeping consumes.
|
|
330
|
+
*
|
|
331
|
+
* The two engines return different envelopes — the SDK read's `PagedResult` + `paging`, the search's flat page —
|
|
332
|
+
* and normalizing HERE rather than per engine is what keeps the continuation, retry, attribution and exhaustion
|
|
333
|
+
* rules literally the same code on both paths. A second copy of those rules per engine is how one path would come
|
|
334
|
+
* to advertise a continuation the other calls terminal.
|
|
335
|
+
*/
|
|
336
|
+
interface OrgIssuesRead {
|
|
337
|
+
/** The page the provider served, or `undefined` when the read returned none (which `warning` explains). */
|
|
338
|
+
value?: { items: IssueShape[]; hasMore: boolean; cursor?: string; truncated: boolean };
|
|
339
|
+
/** The read's own failure. Already included in `warnings`; carried separately because the retry rules key off it. */
|
|
340
|
+
warning?: ProviderWarning;
|
|
341
|
+
/** Everything the engine produced, the read's own failure included. */
|
|
342
|
+
warnings: ProviderWarning[];
|
|
343
|
+
fetchFailed: boolean;
|
|
344
|
+
truncated: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* The engine established there is NOTHING to read for this org — the repository drain returned no
|
|
347
|
+
* repositories — so the slice is terminal and empty. Distinct from `value == null`, which means a read WAS
|
|
348
|
+
* issued and came back with no page, and so is a position to retry.
|
|
349
|
+
*/
|
|
350
|
+
nothingToRead?: boolean;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Whether this org can be read through the FILTERED issue search instead of the repository drain.
|
|
355
|
+
*
|
|
356
|
+
* Two conditions, and the second is not a formality: an empty org name is dropped by the provider's scope
|
|
357
|
+
* translation rather than rejected, which would leave a search of the WHOLE HOST — so the scope is validated
|
|
358
|
+
* through {@link resolveIssueSearchScope}, the same rule `searchIssuesPage` refuses on, rather than by an
|
|
359
|
+
* `org.name.length > 0` written here and free to drift from it.
|
|
360
|
+
*/
|
|
361
|
+
function canSearchOrgIssues(org: ProviderBroadenOrg): boolean {
|
|
362
|
+
return (
|
|
363
|
+
supportsFilteredIssueSearch(org.providerId) &&
|
|
364
|
+
resolveIssueSearchScope(undefined, org.name, undefined).rejection == null
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The org-scoped filtered search: ONE request per page, whatever the org contains.
|
|
370
|
+
*
|
|
371
|
+
* Replaces the drain + SDK read for every provider that declares a filtered issue search — the repository drain
|
|
372
|
+
* costs up to 100 requests per org, and the SDK's repo-scoped read past GitHub's result ceiling enters a
|
|
373
|
+
* per-repository recovery walk that can spend 128 more and still return an incomplete set. Measured on an org
|
|
374
|
+
* past that ceiling: 133 requests and 111s, against this read's 10 and 40s. The search reaches the same scope by
|
|
375
|
+
* NAMING the org, so it needs no repositories at all.
|
|
376
|
+
*
|
|
377
|
+
* It is also the more COMPLETE of the two over the window a consumer reads first, despite returning fewer items —
|
|
378
|
+
* see the module docstring's table for the measurement and why that is not a contradiction.
|
|
379
|
+
*
|
|
380
|
+
* `criteria` is omitted entirely, and each half of that is deliberate:
|
|
381
|
+
* - `relationships` OMITTED is what drops the assignee constraint, which is the substitution `includeAllAssignees:
|
|
382
|
+
* true` performs on the read this replaces. NOT `['any-assignee']`, which means "assigned to somebody" and would
|
|
383
|
+
* exclude every unassigned issue — the opposite of broadening.
|
|
384
|
+
* - every other criterion left unset resolves to the provider's own default, which is the state this read already
|
|
385
|
+
* served (open issues only, archived repositories excluded).
|
|
386
|
+
*
|
|
387
|
+
* At the result ceiling this SUCCEEDS and reports how many matched instead of walking for the rest — see the
|
|
388
|
+
* ceiling warning below.
|
|
389
|
+
*/
|
|
390
|
+
async function readOrgIssuesViaSearch(
|
|
391
|
+
integration: GitHostIntegration,
|
|
392
|
+
org: ProviderBroadenOrg,
|
|
393
|
+
domain: string | undefined,
|
|
394
|
+
cursor: string | undefined,
|
|
395
|
+
): Promise<OrgIssuesRead> {
|
|
396
|
+
const connectionId = org.connectionId;
|
|
397
|
+
const captured = await runCaptured(
|
|
398
|
+
org.providerId,
|
|
399
|
+
domain,
|
|
400
|
+
connectionId,
|
|
401
|
+
() => integration.searchIssuesPageResult({ org: org.name, cursor: cursor }, undefined, connectionId),
|
|
402
|
+
{ warnOnMissingSession: true },
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// A provider that declares the capability but implements no search hook answers `undefined` with no error.
|
|
406
|
+
// Reported as the explicit unsupported failure rather than left as a silent empty page, so it can't be mistaken
|
|
407
|
+
// for an org with no issues — and as the read's `warning`, so the slice treats it as the read failure it is.
|
|
408
|
+
const warning =
|
|
409
|
+
captured.warning ??
|
|
410
|
+
(captured.value == null
|
|
411
|
+
? unsupportedIssueSearchCriteriaWarning(org.providerId, domain, connectionId, {
|
|
412
|
+
reason: 'unsupported-search',
|
|
413
|
+
})
|
|
414
|
+
: undefined);
|
|
415
|
+
const warnings = warning != null ? [warning] : [];
|
|
416
|
+
|
|
417
|
+
let value: OrgIssuesRead['value'];
|
|
418
|
+
if (captured.value != null) {
|
|
419
|
+
const page = captured.value;
|
|
420
|
+
value = {
|
|
421
|
+
items: page.values,
|
|
422
|
+
hasMore: page.hasMore,
|
|
423
|
+
cursor: usableCursor(page.cursor),
|
|
424
|
+
truncated: page.truncated,
|
|
425
|
+
};
|
|
426
|
+
// The result ceiling is the one incompleteness this read can QUANTIFY, so it carries the total rather than
|
|
427
|
+
// a bare "may be incomplete" — which is what lets a consumer say "1.474 matched, showing the 1.000 most
|
|
428
|
+
// recently updated" instead of silently serving a truncated list. Only when nothing else already explained
|
|
429
|
+
// the incompleteness, and only the ceiling gets the number: any other cause falls back to the generic
|
|
430
|
+
// wording rather than reporting a limit it didn't hit. `exhausted`: nothing this read exposes would return
|
|
431
|
+
// the withheld items.
|
|
432
|
+
if (page.truncated && warnings.length === 0) {
|
|
433
|
+
warnings.push(
|
|
434
|
+
issueSearchCapResultWarning(
|
|
435
|
+
org.providerId,
|
|
436
|
+
domain,
|
|
437
|
+
connectionId,
|
|
438
|
+
page.totalCount,
|
|
439
|
+
effectiveIssueSort(undefined),
|
|
440
|
+
) ?? truncationWarning(org.providerId, domain, connectionId, 'Issue search', 'exhausted'),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return { value: value, warning: warning, warnings: warnings, fetchFailed: false, truncated: false };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The original engine: drain the org's repositories, then read the issues across them.
|
|
450
|
+
*
|
|
451
|
+
* Kept for a provider that declares NO filtered issue search (Azure DevOps, GitLab), where refusing the org would
|
|
452
|
+
* be a regression rather than a saving. Its cost, and the arbitrary subset it serves past the result ceiling, are
|
|
453
|
+
* what {@link readOrgIssuesViaSearch} exists to avoid — see the module docstring's measurement. Those providers
|
|
454
|
+
* keep both, because on them there is nothing better to switch to.
|
|
455
|
+
*/
|
|
456
|
+
async function readOrgIssuesViaRepoDrain(
|
|
457
|
+
integration: GitHostIntegration,
|
|
458
|
+
org: ProviderBroadenOrg,
|
|
459
|
+
domain: string | undefined,
|
|
460
|
+
cursor: string | undefined,
|
|
461
|
+
): Promise<OrgIssuesRead> {
|
|
462
|
+
const connectionId = org.connectionId;
|
|
463
|
+
const reposDrain = await drainRepositories(
|
|
464
|
+
integration,
|
|
465
|
+
org.providerId,
|
|
466
|
+
domain,
|
|
467
|
+
org.name,
|
|
468
|
+
undefined,
|
|
469
|
+
connectionId,
|
|
470
|
+
100,
|
|
471
|
+
);
|
|
472
|
+
const warnings: ProviderWarning[] = [...reposDrain.warnings];
|
|
473
|
+
|
|
474
|
+
const repos: ProviderReposInput = reposDrain.repos.map(r => ({ ...r }));
|
|
475
|
+
if (repos.length === 0) {
|
|
476
|
+
return {
|
|
477
|
+
warnings: warnings,
|
|
478
|
+
fetchFailed: reposDrain.fetchFailed,
|
|
479
|
+
truncated: reposDrain.truncated,
|
|
480
|
+
nothingToRead: true,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Broaden = "all visible": drop the assigned-to-me filter so unassigned issues are included.
|
|
485
|
+
const captured = await runCaptured(
|
|
486
|
+
org.providerId,
|
|
487
|
+
domain,
|
|
488
|
+
connectionId,
|
|
489
|
+
() =>
|
|
490
|
+
// Normalized shapes seam, uniform with listIssuesPage.
|
|
491
|
+
integration.getMyIssuesForReposAsShapesResult(
|
|
492
|
+
repos,
|
|
493
|
+
{ includeAllAssignees: true, cursor: cursor },
|
|
494
|
+
connectionId,
|
|
495
|
+
),
|
|
496
|
+
{ warnOnMissingSession: true },
|
|
497
|
+
);
|
|
498
|
+
if (captured.warning != null) {
|
|
499
|
+
warnings.push(captured.warning);
|
|
500
|
+
}
|
|
501
|
+
const assessment = mergeAssessmentInto(warnings, org.providerId, domain, connectionId, captured.value?.metadata);
|
|
502
|
+
|
|
503
|
+
let value: OrgIssuesRead['value'];
|
|
504
|
+
if (captured.value != null) {
|
|
505
|
+
const paged = toProviderPageInfo(captured.value.values.length, captured.value.paging);
|
|
506
|
+
value = {
|
|
507
|
+
items: captured.value.values,
|
|
508
|
+
hasMore: paged.hasMore,
|
|
509
|
+
cursor: paged.cursor,
|
|
510
|
+
// Carry a truncation signal from the issue read too: a provider that couldn't confirm it drained a
|
|
511
|
+
// repo (`paging.truncated`) means this org's issues may be incomplete, on top of any repo-drain
|
|
512
|
+
// truncation already captured above.
|
|
513
|
+
truncated: paged.truncated || assessment.truncated,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
value: value,
|
|
519
|
+
warning: captured.warning,
|
|
520
|
+
warnings: warnings,
|
|
521
|
+
fetchFailed: reposDrain.fetchFailed || assessment.fetchFailed,
|
|
522
|
+
truncated: reposDrain.truncated,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Reads one org's visible issues, through the org-scoped filtered search where the provider has one and through
|
|
528
|
+
* the repository drain where it doesn't. Everything below the engine — the position, the retry slot, the
|
|
529
|
+
* attribution and the exhaustion mark — is the same on both paths.
|
|
530
|
+
*/
|
|
280
531
|
async function readOrgSlice(
|
|
281
532
|
ctx: ProviderReadContext,
|
|
282
533
|
options: BroadenIssuesOptions,
|
|
@@ -362,71 +613,32 @@ async function readOrgSlice(
|
|
|
362
613
|
await ctx.forceRefreshIfRequested(integration, options.forceSync, connectionId);
|
|
363
614
|
|
|
364
615
|
const domain = ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain);
|
|
365
|
-
const reposDrain = await drainRepositories(
|
|
366
|
-
integration,
|
|
367
|
-
org.providerId,
|
|
368
|
-
domain,
|
|
369
|
-
org.name,
|
|
370
|
-
undefined,
|
|
371
|
-
connectionId,
|
|
372
|
-
100,
|
|
373
|
-
);
|
|
374
|
-
const warnings: ProviderWarning[] = [...reposDrain.warnings];
|
|
375
|
-
const fetchFailed = reposDrain.fetchFailed;
|
|
376
|
-
const truncated = reposDrain.truncated;
|
|
377
|
-
|
|
378
|
-
const repos: ProviderReposInput = reposDrain.repos.map(r => ({ ...r }));
|
|
379
|
-
if (repos.length === 0) {
|
|
380
|
-
return barrenSlice(warnings, { fetchFailed: fetchFailed, truncated: truncated });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Broaden = "all visible": drop the assigned-to-me filter so unassigned issues are included.
|
|
384
616
|
const cursor = getBroadenIssuesCursor(options.cursor, org, page, options.orgs.length);
|
|
385
|
-
const
|
|
386
|
-
org
|
|
387
|
-
domain,
|
|
388
|
-
|
|
389
|
-
()
|
|
390
|
-
// Normalized shapes seam, uniform with listIssuesPage.
|
|
391
|
-
integration.getMyIssuesForReposAsShapesResult(
|
|
392
|
-
repos,
|
|
393
|
-
{ includeAllAssignees: true, cursor: cursor },
|
|
394
|
-
connectionId,
|
|
395
|
-
),
|
|
396
|
-
{ warnOnMissingSession: true },
|
|
397
|
-
);
|
|
398
|
-
if (issuesCaptured.warning != null) {
|
|
399
|
-
warnings.push(issuesCaptured.warning);
|
|
617
|
+
const read = canSearchOrgIssues(org)
|
|
618
|
+
? await readOrgIssuesViaSearch(integration, org, domain, cursor)
|
|
619
|
+
: await readOrgIssuesViaRepoDrain(integration, org, domain, cursor);
|
|
620
|
+
if (read.nothingToRead === true) {
|
|
621
|
+
return barrenSlice(read.warnings, { fetchFailed: read.fetchFailed, truncated: read.truncated });
|
|
400
622
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
domain,
|
|
405
|
-
connectionId,
|
|
406
|
-
issuesCaptured.value?.metadata,
|
|
407
|
-
);
|
|
408
|
-
let issuesFetchFailed =
|
|
409
|
-
issuesAssessment.fetchFailed || (issuesCaptured.warning != null && issuesCaptured.value == null);
|
|
623
|
+
|
|
624
|
+
const warnings = read.warnings;
|
|
625
|
+
let issuesFetchFailed = read.fetchFailed || (read.warning != null && read.value == null);
|
|
410
626
|
const items: IssueShape[] = [];
|
|
411
627
|
let hasMore = false;
|
|
412
628
|
let nextCursor: string | undefined;
|
|
413
629
|
let retryPage: number | undefined;
|
|
414
|
-
// Carry a truncation signal from the issue read too: a provider that couldn't confirm it drained
|
|
415
|
-
// a repo (`paging.truncated`) means this org's issues may be incomplete, on top of any repo-drain
|
|
416
|
-
// truncation already captured above.
|
|
417
630
|
let issuesTruncated = false;
|
|
418
|
-
if (
|
|
419
|
-
items.push(...
|
|
420
|
-
const paged = toProviderPageInfo(issuesCaptured.value.values.length, issuesCaptured.value.paging);
|
|
631
|
+
if (read.value != null) {
|
|
632
|
+
items.push(...read.value.items);
|
|
421
633
|
// An org that reports another page but no usable cursor can't be resumed: it would neither be
|
|
422
634
|
// recorded in the composite cursor nor marked exhausted, so the next round would re-read its
|
|
423
635
|
// page 1 and repeat every issue. Treat it as terminal-but-incomplete (which also marks the org
|
|
424
636
|
// exhausted below, since `exhausted` keys off `!hasMore`).
|
|
425
|
-
const continuation = resolveContinuation(
|
|
637
|
+
const continuation = resolveContinuation(read.value, undefined);
|
|
426
638
|
hasMore = continuation.hasMore;
|
|
427
639
|
nextCursor = continuation.cursor;
|
|
428
|
-
issuesTruncated = continuation.truncated
|
|
429
|
-
} else if (
|
|
640
|
+
issuesTruncated = continuation.truncated;
|
|
641
|
+
} else if (read.warning != null || cursor != null) {
|
|
430
642
|
// Keep the exact position that failed. Without this retry cursor a multi-org continuation would
|
|
431
643
|
// omit this org from the bundle, then synthesize the next numbered page and silently skip the
|
|
432
644
|
// failed page. The synthesized page cursor is also actionable for a first-page cursor-only read:
|
|
@@ -435,7 +647,7 @@ async function readOrgSlice(
|
|
|
435
647
|
// query request it forever. Healthy sibling continuations set `hasMore` independently.
|
|
436
648
|
if (cursor != null) {
|
|
437
649
|
nextCursor = cursor;
|
|
438
|
-
if (
|
|
650
|
+
if (read.warning == null) {
|
|
439
651
|
appendDedupedWarning(
|
|
440
652
|
warnings,
|
|
441
653
|
otherWarning(
|
|
@@ -456,7 +668,7 @@ async function readOrgSlice(
|
|
|
456
668
|
return {
|
|
457
669
|
items: items,
|
|
458
670
|
warnings: warnings,
|
|
459
|
-
broadenedProviderIds:
|
|
671
|
+
broadenedProviderIds: read.value != null ? [org.providerId] : [],
|
|
460
672
|
providerId: org.providerId,
|
|
461
673
|
org: org.name,
|
|
462
674
|
connectionId: connectionId,
|
|
@@ -466,8 +678,8 @@ async function readOrgSlice(
|
|
|
466
678
|
hasMore: hasMore,
|
|
467
679
|
// Exhausted once a successful read reports no more pages — recorded in the cursor so later
|
|
468
680
|
// rounds skip it while other orgs keep paging.
|
|
469
|
-
exhausted:
|
|
470
|
-
fetchFailed:
|
|
471
|
-
truncated: truncated || issuesTruncated,
|
|
681
|
+
exhausted: read.value != null && !hasMore,
|
|
682
|
+
fetchFailed: issuesFetchFailed,
|
|
683
|
+
truncated: read.truncated || issuesTruncated,
|
|
472
684
|
};
|
|
473
685
|
}
|
|
@@ -196,6 +196,20 @@ export type IssueSearchCriteriaRejection =
|
|
|
196
196
|
/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */
|
|
197
197
|
| { reason: 'contradictory-relationships' };
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE
|
|
201
|
+
* building criteria, so a read that would only be refused is never issued.
|
|
202
|
+
*
|
|
203
|
+
* The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather
|
|
204
|
+
* than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to
|
|
205
|
+
* disagree with the validator about what "has a search" means. `broadenIssues` reads it to pick its engine —
|
|
206
|
+
* the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather
|
|
207
|
+
* than a refusal, so it needs the predicate without the rejection.
|
|
208
|
+
*/
|
|
209
|
+
export function supportsFilteredIssueSearch(id: IntegrationIds): boolean {
|
|
210
|
+
return providersMetadata[id]?.supportedIssueSearch != null;
|
|
211
|
+
}
|
|
212
|
+
|
|
199
213
|
/**
|
|
200
214
|
* Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.
|
|
201
215
|
*
|
|
@@ -311,6 +325,23 @@ export type IssueSearchScopeRejection =
|
|
|
311
325
|
/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */
|
|
312
326
|
| 'repo-ids';
|
|
313
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Whether a scope name survives the provider's sanitizing, i.e. whether it will actually constrain the query.
|
|
330
|
+
*
|
|
331
|
+
* Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this
|
|
332
|
+
* 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. What is common to any query language is the part that matters: a value made only of
|
|
334
|
+
* whitespace, quotes and control characters carries no name, and every sanitizer strips exactly those (quotes
|
|
335
|
+
* because they would close their own qualifier, control characters because they cannot appear in a query at all).
|
|
336
|
+
*
|
|
337
|
+
* Strictly weaker than any provider's own sanitizing, which is the safe direction: this can only pass a name the
|
|
338
|
+
* provider would then narrow further, never reject one the provider would have accepted.
|
|
339
|
+
*/
|
|
340
|
+
function isUsableSearchScopeName(name: string): boolean {
|
|
341
|
+
// eslint-disable-next-line no-control-regex
|
|
342
|
+
return name.replace(/["\u0000-\u001f\u007f\s]/g, '').length > 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
314
345
|
/**
|
|
315
346
|
* Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the
|
|
316
347
|
* provider query needs.
|
|
@@ -322,6 +353,13 @@ export type IssueSearchScopeRejection =
|
|
|
322
353
|
*
|
|
323
354
|
* The two rejections are mutually exclusive — `repo-ids` requires repositories and `unscoped` requires none — so
|
|
324
355
|
* the order they're checked in cannot change the outcome.
|
|
356
|
+
*
|
|
357
|
+
* An org is checked for what SURVIVES SANITIZING, not merely for being non-empty, and that distinction is a
|
|
358
|
+
* SECURITY one rather than a nicety: the provider query drops a value that sanitizes away rather than rejecting
|
|
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.
|
|
325
363
|
*/
|
|
326
364
|
export function resolveIssueSearchScope(
|
|
327
365
|
repos: ProviderReposInput | undefined,
|
|
@@ -335,7 +373,7 @@ export function resolveIssueSearchScope(
|
|
|
335
373
|
return { repos: repos as ProviderRepoInput[] };
|
|
336
374
|
}
|
|
337
375
|
|
|
338
|
-
if (org != null && org
|
|
376
|
+
if (org != null && isUsableSearchScopeName(org)) return {};
|
|
339
377
|
if (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};
|
|
340
378
|
|
|
341
379
|
return { rejection: 'unscoped' };
|