@gitkraken/core-gitlens 0.5.112 → 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 +19 -3
- 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 +50 -1
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +333 -29
- 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 +15 -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 +43 -0
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +49 -17
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +40 -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 +10 -0
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +8 -0
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +2 -1
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.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/pullRequests.d.ts +6 -0
- package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +28 -40
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.d.ts +2 -0
- package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +1 -0
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- 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 +12 -4
- 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 +381 -31
- 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 +19 -0
- package/src/plus/integrations/manager.ts +43 -0
- package/src/plus/integrations/models/gitHostIntegration.ts +77 -18
- package/src/plus/integrations/models/integration.ts +1 -0
- package/src/plus/integrations/providers/github.ts +20 -0
- package/src/plus/integrations/providers/models.ts +6 -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/pullRequests.ts +51 -70
- package/src/plus/integrations/reads/searchPullRequests.ts +3 -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
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
gitHubIssueSearchRelationships,
|
|
81
81
|
toGitHubIssueSearchQualifiers,
|
|
82
82
|
toGitHubIssueSearchScopeQualifiers,
|
|
83
|
+
toGitHubIssueSearchSlideQualifier,
|
|
83
84
|
toGitHubIssueSortQualifier,
|
|
84
85
|
} from './issueSearchQuery.js';
|
|
85
86
|
import { toGitHubPullRequestSearchFacets } from './pullRequestSearchQuery.js';
|
|
@@ -114,7 +115,8 @@ const accountResolveBatchSize = 25;
|
|
|
114
115
|
// exceeded` on large repositories. Thirty keeps the default within that GraphQL cost budget;
|
|
115
116
|
// the 100-node maximum the connection accepts is for the lite shape, whose per-node cost is a
|
|
116
117
|
// fraction of it. The budget is per DOCUMENT, and one document holds every relationship × state
|
|
117
|
-
// facet, so the worst case scales with the facet count (five relationships ×
|
|
118
|
+
// facet, so the worst case scales with the facet count (five relationships × three states = 15;
|
|
119
|
+
// `all` subsumes the concrete states).
|
|
118
120
|
const defaultPullRequestSearchPageSize = 30;
|
|
119
121
|
const maxPullRequestSearchPageSize = 100;
|
|
120
122
|
// Pages `searchMyPullRequests` drains for a caller that wants a whole list rather than a page.
|
|
@@ -181,6 +183,52 @@ interface AliasedIssueSearch {
|
|
|
181
183
|
query: string;
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The sort value of the last item on a page, which is the boundary a ceiling slide continues from.
|
|
188
|
+
*
|
|
189
|
+
* Reads the DATE off the issue rather than taking a caller-supplied position, so the boundary can only ever be a
|
|
190
|
+
* point this walk actually reached. Returns `undefined` for an empty page or a key carrying no date, which is the
|
|
191
|
+
* signal to report the ceiling instead of sliding.
|
|
192
|
+
*/
|
|
193
|
+
function lastSortBoundary(issues: readonly IssueShape[], sort: IssueSorting): Date | undefined {
|
|
194
|
+
const last = issues.at(-1);
|
|
195
|
+
if (last == null) return undefined;
|
|
196
|
+
|
|
197
|
+
const value = sort.startsWith('created:')
|
|
198
|
+
? last.createdDate
|
|
199
|
+
: sort.startsWith('updated:')
|
|
200
|
+
? last.updatedDate
|
|
201
|
+
: undefined;
|
|
202
|
+
return value instanceof Date && !Number.isNaN(value.getTime()) ? value : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The per-alias ceiling-slide state, read defensively off a composite cursor.
|
|
207
|
+
*
|
|
208
|
+
* Keys are `<field>:<alias>`. The colon is what makes them collision-proof: a GraphQL alias is a name (a letter
|
|
209
|
+
* or underscore followed by letters, digits or underscores) and so can never contain one, which is why these need
|
|
210
|
+
* no entry in the reserved-alias check and that list does not have to grow with every field added here.
|
|
211
|
+
*
|
|
212
|
+
* Everything is validated to the shape this module emits, because a cursor is caller-supplied data on a round
|
|
213
|
+
* trip: an unrecognized value restarts the walk instead of reaching the query, which is the difference between a
|
|
214
|
+
* repeated page and an attacker-chosen qualifier re-scoping someone's search.
|
|
215
|
+
*/
|
|
216
|
+
function cursorString(cursor: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
217
|
+
const value = cursor?.[key];
|
|
218
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cursorNumber(cursor: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
222
|
+
const value = cursor?.[key];
|
|
223
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The urls a prior page already served at this alias's inclusive slide boundary. */
|
|
227
|
+
function slideSeenFor(cursor: Record<string, unknown> | undefined, alias: string): Set<string> | undefined {
|
|
228
|
+
const value = cursorString(cursor, `slideSeen:${alias}`);
|
|
229
|
+
return value != null ? new Set(value.split(' ')) : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
184
232
|
/** One page of a multi-search issue request, with its composite cursor across every alias. */
|
|
185
233
|
export interface AliasedIssueSearchResult {
|
|
186
234
|
values: IssueShape[];
|
|
@@ -3294,6 +3342,18 @@ export class GitHubApi {
|
|
|
3294
3342
|
variables: RequestParameters,
|
|
3295
3343
|
scope: ScopedLogger | undefined,
|
|
3296
3344
|
cancellation?: AbortSignal | undefined,
|
|
3345
|
+
/**
|
|
3346
|
+
* Accept a response whose only failures are NOT_FOUND, returning its PARTIAL data instead of throwing.
|
|
3347
|
+
*
|
|
3348
|
+
* For a single-entity query a NOT_FOUND is the whole answer, so throwing is right. For an ALIASED batch it
|
|
3349
|
+
* is one slot's answer: GitHub replies 200 with every resolvable alias populated and a NOT_FOUND per
|
|
3350
|
+
* missing one, so throwing discards the results that did resolve. Since a missing coordinate is the common
|
|
3351
|
+
* case for a batch, that would make the read useless for the very question it answers.
|
|
3352
|
+
*
|
|
3353
|
+
* Narrow on purpose: any error that is NOT a NOT_FOUND still throws, so auth, rate-limit and query-cost
|
|
3354
|
+
* failures keep their existing handling rather than being silently reported as a batch of absences.
|
|
3355
|
+
*/
|
|
3356
|
+
allowPartialNotFound?: boolean,
|
|
3297
3357
|
): Promise<T | undefined> {
|
|
3298
3358
|
const { accessToken, ...tokenInfo } = token;
|
|
3299
3359
|
// Only dedupe when no cancellation/request option is in play — sharing a promise that
|
|
@@ -3338,6 +3398,16 @@ export class GitHubApi {
|
|
|
3338
3398
|
);
|
|
3339
3399
|
} catch (ex) {
|
|
3340
3400
|
if (ex instanceof GraphqlResponseError) {
|
|
3401
|
+
// `every`, not `[0]`: a batch can report several, and one non-NOT_FOUND among them is a real
|
|
3402
|
+
// failure that must not be reported as a set of absences.
|
|
3403
|
+
if (
|
|
3404
|
+
allowPartialNotFound &&
|
|
3405
|
+
ex.data != null &&
|
|
3406
|
+
(ex.errors?.every(e => e.type === 'NOT_FOUND') ?? false)
|
|
3407
|
+
) {
|
|
3408
|
+
return ex.data as T;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3341
3411
|
switch (ex.errors?.[0]?.type) {
|
|
3342
3412
|
case 'NOT_FOUND':
|
|
3343
3413
|
throw new RequestNotFoundError(ex);
|
|
@@ -3958,15 +4028,29 @@ export class GitHubApi {
|
|
|
3958
4028
|
// - the ORDER is assigned → mentioned → authored, which is the order the union is emitted in and, because
|
|
3959
4029
|
// the dedupe keeps the first occurrence of a url, also the precedence between the three. An issue that
|
|
3960
4030
|
// is both assigned to and authored by the user surfaces as the assigned one.
|
|
4031
|
+
// The per-alias ceiling-slide bound a prior page sealed. Applied HERE because this method owns its query
|
|
4032
|
+
// text: without it the bound would be recorded and never honoured, so the continuation would re-issue the
|
|
4033
|
+
// unbounded query, serve the same first 1.000, and have every one of them filtered out as already-seen —
|
|
4034
|
+
// a page of nothing, repeated until the walk gave up (measured: 20 requests to do 10 requests' work).
|
|
4035
|
+
const bound = this.parseSlidesFromCursor(options?.cursor);
|
|
3961
4036
|
const searches: AliasedIssueSearch[] = [];
|
|
3962
4037
|
if (requested.assigned) {
|
|
3963
|
-
searches.push({
|
|
4038
|
+
searches.push({
|
|
4039
|
+
alias: 'assigned',
|
|
4040
|
+
query: `${search} ${baseFilters} ${assignedQualifier} ${bound('assigned') ?? ''}`.trim(),
|
|
4041
|
+
});
|
|
3964
4042
|
}
|
|
3965
4043
|
if (requested.mentioned) {
|
|
3966
|
-
searches.push({
|
|
4044
|
+
searches.push({
|
|
4045
|
+
alias: 'mentioned',
|
|
4046
|
+
query: `${search} ${baseFilters} mentions:@me ${bound('mentioned') ?? ''}`.trim(),
|
|
4047
|
+
});
|
|
3967
4048
|
}
|
|
3968
4049
|
if (requested.authored) {
|
|
3969
|
-
searches.push({
|
|
4050
|
+
searches.push({
|
|
4051
|
+
alias: 'authored',
|
|
4052
|
+
query: `${search} ${baseFilters} author:@me ${bound('authored') ?? ''}`.trim(),
|
|
4053
|
+
});
|
|
3970
4054
|
}
|
|
3971
4055
|
|
|
3972
4056
|
// Field by field, like `searchIssuesPage`: `options` also carries `repos`/`includeAllAssignees`/`categories`,
|
|
@@ -4002,7 +4086,15 @@ export class GitHubApi {
|
|
|
4002
4086
|
*
|
|
4003
4087
|
* With more than one relationship the page is a UNION of several searches, each ordered by the provider; the
|
|
4004
4088
|
* merged page is re-sorted here so the whole page honors the requested key. Across pages the order is still
|
|
4005
|
-
* per-alias — see {@link searchIssuesByAlias}.
|
|
4089
|
+
* per-alias — see {@link searchIssuesByAlias}. Note a union can also serve the same issue on two pages when
|
|
4090
|
+
* its relationships OVERLAP (an issue both authored by and assigned to the user sits at different depths in
|
|
4091
|
+
* the two), which predates the ceiling slide and is a property of unioning independent walks: measured at 50
|
|
4092
|
+
* repeats over 450 issues with no ceiling involved. Dedupe by url if that matters to the surface.
|
|
4093
|
+
*
|
|
4094
|
+
* GitHub's per-query result ceiling is NOT terminal here: an alias that runs out of pages while capped
|
|
4095
|
+
* continues bounded to the far side of its own last item, which is a fresh query with a fresh budget. Bounded
|
|
4096
|
+
* per alias, since they exhaust independently. Only a ceiling an alias cannot slide past (a sort key with no
|
|
4097
|
+
* usable range qualifier) is reported as an omission.
|
|
4006
4098
|
*
|
|
4007
4099
|
* Each requested relationship becomes its own aliased search, unioned and deduped by url; with none, a single
|
|
4008
4100
|
* search runs over the scope alone. `criteria.text` and the other free-form values are sanitized so user input
|
|
@@ -4031,17 +4123,22 @@ export class GitHubApi {
|
|
|
4031
4123
|
...toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos),
|
|
4032
4124
|
...toGitHubIssueSearchQualifiers(options?.criteria, sort),
|
|
4033
4125
|
].join(' ');
|
|
4126
|
+
// A cursor from a walk that already slid past the ceiling carries the bound each alias slid to, and the
|
|
4127
|
+
// query has to be rebuilt with it or that alias would restart at the top and re-serve its first 1.000.
|
|
4128
|
+
// PER ALIAS, because they slide independently: one bound shared across them would sit below where an
|
|
4129
|
+
// early-finishing alias stopped and re-serve everything it had already delivered.
|
|
4130
|
+
const bound = this.parseSlidesFromCursor(options?.cursor);
|
|
4034
4131
|
|
|
4035
4132
|
// One aliased search per relationship, OR-ed by union. They can't be one query: GitHub AND-s qualifiers,
|
|
4036
4133
|
// so `author:@me assignee:@me` would return the intersection — issues the user both opened and is assigned
|
|
4037
4134
|
// to — instead of either set. With no relationship the scope + criteria are already the whole query.
|
|
4038
4135
|
const relationships = options?.criteria?.relationships;
|
|
4039
4136
|
const searches: AliasedIssueSearch[] = relationships?.length
|
|
4040
|
-
? relationships.map(r =>
|
|
4041
|
-
alias
|
|
4042
|
-
query: `${base} ${
|
|
4043
|
-
})
|
|
4044
|
-
: [{ alias: 'matched', query: base }];
|
|
4137
|
+
? relationships.map(r => {
|
|
4138
|
+
const { alias, qualifier } = gitHubIssueSearchRelationships[r];
|
|
4139
|
+
return { alias: alias, query: `${base} ${qualifier} ${bound(alias) ?? ''}`.trim() };
|
|
4140
|
+
})
|
|
4141
|
+
: [{ alias: 'matched', query: `${base} ${bound('matched') ?? ''}`.trim() }];
|
|
4045
4142
|
|
|
4046
4143
|
// Forwarded field by field rather than spread: `options` also carries `repos`/`org`/`criteria`, which are
|
|
4047
4144
|
// already baked into `searches[].query` above and which the callee declares nothing about. `sort` is the
|
|
@@ -4066,6 +4163,32 @@ export class GitHubApi {
|
|
|
4066
4163
|
);
|
|
4067
4164
|
}
|
|
4068
4165
|
|
|
4166
|
+
/**
|
|
4167
|
+
* Reads back the per-alias ceiling-slide bounds a prior page sealed into its cursor.
|
|
4168
|
+
*
|
|
4169
|
+
* Each value is validated against the EXACT shape {@link toGitHubIssueSearchSlideQualifier} emits, not merely
|
|
4170
|
+
* checked for being a string. A cursor is caller-supplied data on a round trip, so an unvalidated value here
|
|
4171
|
+
* would be a qualifier of someone else's choosing appended to the search: `org:evil-corp` in a cursor would
|
|
4172
|
+
* re-scope the read. Anything unrecognized yields no bound, which restarts that alias at the top — a repeated
|
|
4173
|
+
* page, which is the safe failure.
|
|
4174
|
+
*/
|
|
4175
|
+
private parseSlidesFromCursor(cursor: string | undefined): (alias: string) => string | undefined {
|
|
4176
|
+
let parsed: Record<string, unknown> | undefined;
|
|
4177
|
+
if (cursor != null) {
|
|
4178
|
+
try {
|
|
4179
|
+
parsed = JSON.parse(cursor) as Record<string, unknown>;
|
|
4180
|
+
} catch {}
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
return alias => {
|
|
4184
|
+
const slide = parsed?.[`slide:${alias}`];
|
|
4185
|
+
return typeof slide === 'string' &&
|
|
4186
|
+
/^(?:created|updated):(?:<=|>=)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(slide)
|
|
4187
|
+
? slide
|
|
4188
|
+
: undefined;
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4069
4192
|
/**
|
|
4070
4193
|
* Counts issues for several scopes in ONE request, transferring no issues at all — each scope is an aliased
|
|
4071
4194
|
* `search` selecting only `issueCount`, with `first: 0` so no nodes are fetched.
|
|
@@ -4142,6 +4265,98 @@ export class GitHubApi {
|
|
|
4142
4265
|
}
|
|
4143
4266
|
}
|
|
4144
4267
|
|
|
4268
|
+
/**
|
|
4269
|
+
* Resolves several issues BY COORDINATE — `(owner, repo, number)` — in one request, via aliased `repository`
|
|
4270
|
+
* fields rather than aliased searches.
|
|
4271
|
+
*
|
|
4272
|
+
* Aliasing the point read rather than {@link searchIssuesByAlias} is the whole design, and each difference
|
|
4273
|
+
* matters to the caller this exists for (correlating a branch name to the issue it references):
|
|
4274
|
+
* - it resolves by EXACT NUMBER, where a search answers a relevance question;
|
|
4275
|
+
* - no result ceiling applies, so there is no partial window to reason about;
|
|
4276
|
+
* - and a null is a PROVEN ABSENCE rather than "not found within a page budget", which is what makes a miss
|
|
4277
|
+
* cacheable. That last one is the point: a caller that cannot prove absence re-walks its budget forever.
|
|
4278
|
+
*
|
|
4279
|
+
* Returns POSITIONALLY — one slot per input coordinate, in order — for the same reason {@link countIssues}
|
|
4280
|
+
* does: a caller's key is arbitrary text and would break the GraphQL document, so the aliases are generated
|
|
4281
|
+
* and the caller maps back by index. `undefined` in a slot means the issue does not exist (or is not visible
|
|
4282
|
+
* to this token), never that the read failed; a failure throws.
|
|
4283
|
+
*/
|
|
4284
|
+
@trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
4285
|
+
async getIssuesBatch(
|
|
4286
|
+
provider: Provider,
|
|
4287
|
+
token: GitHubTokenInfo,
|
|
4288
|
+
coordinates: readonly { owner: string; repo: string; number: number }[],
|
|
4289
|
+
options?: { baseUrl?: string; avatarSize?: number; includeBody?: boolean },
|
|
4290
|
+
cancellation?: AbortSignal,
|
|
4291
|
+
): Promise<(IssueShape | undefined)[]> {
|
|
4292
|
+
const scope = getScopedLogger();
|
|
4293
|
+
if (coordinates.length === 0) return [];
|
|
4294
|
+
|
|
4295
|
+
const params = coordinates
|
|
4296
|
+
.map((_, i) => `$o${i}: String!\n\t\t\t\t$n${i}: String!\n\t\t\t\t$k${i}: Int!`)
|
|
4297
|
+
.join('\n\t\t\t\t');
|
|
4298
|
+
const fields = coordinates
|
|
4299
|
+
.map(
|
|
4300
|
+
(_, i) => `i${i}: repository(owner: $o${i}, name: $n${i}) {
|
|
4301
|
+
issue(number: $k${i}) {
|
|
4302
|
+
${gqIssueFragment}${options?.includeBody ? '\n\t\t\t\t\t\tbody' : ''}
|
|
4303
|
+
}
|
|
4304
|
+
}`,
|
|
4305
|
+
)
|
|
4306
|
+
.join('\n\t\t\t\t');
|
|
4307
|
+
const query = `query getIssuesBatch(
|
|
4308
|
+
${params}
|
|
4309
|
+
$avatarSize: Int
|
|
4310
|
+
) {
|
|
4311
|
+
${fields}
|
|
4312
|
+
}`;
|
|
4313
|
+
|
|
4314
|
+
const variables: Record<string, unknown> = {
|
|
4315
|
+
baseUrl: options?.baseUrl,
|
|
4316
|
+
avatarSize: options?.avatarSize,
|
|
4317
|
+
};
|
|
4318
|
+
coordinates.forEach((c, i) => {
|
|
4319
|
+
variables[`o${i}`] = c.owner;
|
|
4320
|
+
variables[`n${i}`] = c.repo;
|
|
4321
|
+
variables[`k${i}`] = c.number;
|
|
4322
|
+
});
|
|
4323
|
+
|
|
4324
|
+
try {
|
|
4325
|
+
// `allowPartialNotFound`: a coordinate that does not exist is this read's ANSWER for that slot, not a
|
|
4326
|
+
// failure of the batch. GitHub replies 200 with the resolvable aliases populated and a NOT_FOUND per
|
|
4327
|
+
// missing one, so without this a single bad coordinate would discard every good result — and a miss
|
|
4328
|
+
// is the common outcome for the caller this read exists for.
|
|
4329
|
+
const rsp = await this.graphql<Record<string, { issue?: GitHubIssue | null } | null | undefined>>(
|
|
4330
|
+
provider,
|
|
4331
|
+
token,
|
|
4332
|
+
query,
|
|
4333
|
+
variables,
|
|
4334
|
+
scope,
|
|
4335
|
+
cancellation,
|
|
4336
|
+
true,
|
|
4337
|
+
);
|
|
4338
|
+
if (rsp == null) return coordinates.map(() => undefined);
|
|
4339
|
+
|
|
4340
|
+
// Mapped slot by slot so one unmappable issue can't discard the whole batch, matching the aliased
|
|
4341
|
+
// search's node-by-node mapping. An unmappable issue reads as absent, which is the safe direction
|
|
4342
|
+
// here only because the caller is asking "does this exist", and a false absent costs a re-read
|
|
4343
|
+
// rather than a wrong issue.
|
|
4344
|
+
return coordinates.map((c, i) => {
|
|
4345
|
+
const node = rsp[`i${i}`]?.issue;
|
|
4346
|
+
if (node == null) return undefined;
|
|
4347
|
+
|
|
4348
|
+
try {
|
|
4349
|
+
return fromGitHubIssue(node, provider);
|
|
4350
|
+
} catch (ex) {
|
|
4351
|
+
scope?.warn(`skipped unmappable issue; ${c.owner}/${c.repo}#${c.number}, ex=${ex}`);
|
|
4352
|
+
return undefined;
|
|
4353
|
+
}
|
|
4354
|
+
});
|
|
4355
|
+
} catch (ex) {
|
|
4356
|
+
throw this.handleException(ex, provider, scope);
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4145
4360
|
/**
|
|
4146
4361
|
* The PR twin of {@link countIssues}: counts pull requests for several scopes in ONE request via aliased
|
|
4147
4362
|
* `search` fields selecting only `issueCount` with `first: 0`. Same positional/undefined contract as the issue
|
|
@@ -4280,6 +4495,15 @@ export class GitHubApi {
|
|
|
4280
4495
|
interface SearchCursor {
|
|
4281
4496
|
page?: number;
|
|
4282
4497
|
truncated?: boolean;
|
|
4498
|
+
/**
|
|
4499
|
+
* Per-alias ceiling-slide state lives under `slide:<alias>`, `slideTs:<alias>` and
|
|
4500
|
+
* `slideSeen:<alias>`, which the index signature below already covers. Keyed with a colon so they
|
|
4501
|
+
* cannot collide with an alias slot, since a GraphQL alias cannot contain one — see the cursor
|
|
4502
|
+
* helpers above.
|
|
4503
|
+
*
|
|
4504
|
+
* `slideSeen:<alias>` is a space-delimited string rather than an array because this object's index
|
|
4505
|
+
* signature holds primitives, and a url cannot contain whitespace, so the separator is unambiguous.
|
|
4506
|
+
*/
|
|
4283
4507
|
/**
|
|
4284
4508
|
* The order this cursor's pages were produced under: an `IssueSorting`, or `unsortedCursorSort` when
|
|
4285
4509
|
* the caller asked for none. Written as a value rather than left absent in the no-order case
|
|
@@ -4418,19 +4642,31 @@ export class GitHubApi {
|
|
|
4418
4642
|
);
|
|
4419
4643
|
if (rsp == null) return { values: [], hasMore: false, page: page, truncated: false };
|
|
4420
4644
|
|
|
4421
|
-
// Map node-by-node so one unmappable issue can't discard the whole result set
|
|
4422
|
-
|
|
4645
|
+
// Map node-by-node so one unmappable issue can't discard the whole result set. Kept PER ALIAS rather
|
|
4646
|
+
// than flattened straight away: a ceiling slide bounds each alias at its OWN last item, so the
|
|
4647
|
+
// boundary has to be read off that alias's own page and not off the merged one (see below).
|
|
4648
|
+
const issuesByAlias = new Map<string, IssueShape[]>();
|
|
4423
4649
|
for (const s of active) {
|
|
4650
|
+
const mapped: IssueShape[] = [];
|
|
4651
|
+
// Drop what a previous page already served at this alias's INCLUSIVE slide boundary. Applied
|
|
4652
|
+
// before the merge so the dedupe's precedence rules only ever see genuinely new items.
|
|
4653
|
+
const seen = slideSeenFor(cursor, s.alias);
|
|
4424
4654
|
for (const node of rsp[s.alias]?.nodes ?? []) {
|
|
4425
4655
|
if (node?.id == null) continue;
|
|
4426
4656
|
|
|
4427
4657
|
try {
|
|
4428
|
-
|
|
4658
|
+
const issue = fromGitHubIssue(node, provider);
|
|
4659
|
+
if (seen?.has(issue.url) !== true) {
|
|
4660
|
+
mapped.push(issue);
|
|
4661
|
+
}
|
|
4429
4662
|
} catch (ex) {
|
|
4430
4663
|
scope?.warn(`skipped unmappable issue; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
4431
4664
|
}
|
|
4432
4665
|
}
|
|
4666
|
+
issuesByAlias.set(s.alias, mapped);
|
|
4433
4667
|
}
|
|
4668
|
+
// Flattened in `active` order, which is `searches` order, so the dedupe's precedence is unchanged.
|
|
4669
|
+
const issues = active.flatMap(s => issuesByAlias.get(s.alias) ?? []);
|
|
4434
4670
|
|
|
4435
4671
|
// Dedupe by `url`, not `IssueShape.id`: for some providers `id` is a per-repository number, so an
|
|
4436
4672
|
// id-keyed map would collapse distinct issues across repositories.
|
|
@@ -4469,30 +4705,124 @@ export class GitHubApi {
|
|
|
4469
4705
|
let hasMore = false;
|
|
4470
4706
|
let continuationMissing = false;
|
|
4471
4707
|
let maxIssueCount = 0;
|
|
4708
|
+
// Whether ANY alias slid this round. A slide is forward progress, so it makes the read continuable
|
|
4709
|
+
// exactly as an ordinary next-cursor does.
|
|
4710
|
+
let slid = false;
|
|
4711
|
+
// Whether any alias ran out of pages against a capped query WITHOUT being able to slide, which is the
|
|
4712
|
+
// only remaining way this read leaves results unreachable.
|
|
4713
|
+
let strandedByCeiling = false;
|
|
4714
|
+
const sortKey = options.sort;
|
|
4472
4715
|
for (const s of searches) {
|
|
4473
4716
|
const category = rsp[s.alias];
|
|
4474
4717
|
const endCursor =
|
|
4475
4718
|
category?.pageInfo?.hasNextPage && category.pageInfo.endCursor ? category.pageInfo.endCursor : null;
|
|
4476
|
-
next[s.alias] = endCursor;
|
|
4477
|
-
if (endCursor != null) {
|
|
4478
|
-
hasMore = true;
|
|
4479
|
-
}
|
|
4480
4719
|
if (category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null) {
|
|
4481
4720
|
continuationMissing = true;
|
|
4482
4721
|
}
|
|
4483
|
-
|
|
4722
|
+
const issueCount = category?.issueCount ?? 0;
|
|
4723
|
+
maxIssueCount = Math.max(maxIssueCount, issueCount);
|
|
4724
|
+
|
|
4725
|
+
// The urls already served at THIS alias's trailing timestamp, which its inclusive slide re-serves.
|
|
4726
|
+
// Accumulated across pages while that timestamp holds, and reset when it moves, so it carries one
|
|
4727
|
+
// second's worth of urls rather than the walk's — a tied block from a bulk edit can span several
|
|
4728
|
+
// pages, and rebuilding this from the last page alone would re-serve the earlier ones.
|
|
4729
|
+
const mapped = issuesByAlias.get(s.alias);
|
|
4730
|
+
const trailingTs =
|
|
4731
|
+
mapped != null ? lastSortBoundary(mapped, sortKey ?? defaultIssueSort)?.getTime() : undefined;
|
|
4732
|
+
const trailing = new Set<string>(
|
|
4733
|
+
trailingTs != null && cursorNumber(cursor, `slideTs:${s.alias}`) === trailingTs
|
|
4734
|
+
? (slideSeenFor(cursor, s.alias) ?? [])
|
|
4735
|
+
: [],
|
|
4736
|
+
);
|
|
4737
|
+
// Accumulated only while this alias is CAPPED, since only a capped alias can reach a slide. Without
|
|
4738
|
+
// that test every page of every search would seal its trailing second's urls for a slide that
|
|
4739
|
+
// will never happen — a rounding error for an ordinary search, where a second holds one or two
|
|
4740
|
+
// issues, but this rides in a cursor `broadenIssues` nests once per org.
|
|
4741
|
+
//
|
|
4742
|
+
// The carry-forward below is NOT gated the same way: a slid query's remainder is often under the
|
|
4743
|
+
// cap, and its first page still needs the filter for the boundary second the inclusive bound
|
|
4744
|
+
// deliberately re-serves. Gating both re-served that block (measured: 100 duplicates).
|
|
4745
|
+
//
|
|
4746
|
+
// Bounded by the reachable window: one second's worth of urls drawn from at most
|
|
4747
|
+
// `githubSearchResultLimit` items, so ~45 KB per alias at the very worst, which takes a bulk edit
|
|
4748
|
+
// stamping a thousand issues with a single timestamp.
|
|
4749
|
+
const carryBoundary = issueCount > githubSearchResultLimit;
|
|
4750
|
+
if (trailingTs != null && carryBoundary) {
|
|
4751
|
+
for (const issue of mapped ?? []) {
|
|
4752
|
+
if (lastSortBoundary([issue], sortKey ?? defaultIssueSort)?.getTime() === trailingTs) {
|
|
4753
|
+
trailing.add(issue.url);
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
next[`slideTs:${s.alias}`] = trailingTs;
|
|
4757
|
+
next[`slideSeen:${s.alias}`] = [...trailing].join(' ');
|
|
4758
|
+
} else {
|
|
4759
|
+
// A page can come back EMPTY right after a slide: the re-issued query restarts at the top of
|
|
4760
|
+
// the bounded range, so its first page can be entirely items this set already filtered out.
|
|
4761
|
+
// Dropping the set here would un-filter them on the page after, so carry it forward untouched.
|
|
4762
|
+
next[`slideTs:${s.alias}`] = cursorNumber(cursor, `slideTs:${s.alias}`);
|
|
4763
|
+
next[`slideSeen:${s.alias}`] = cursorString(cursor, `slideSeen:${s.alias}`);
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
// A CEILING SLIDE turns the per-query cap from terminal into continuable: re-issuing THIS alias's
|
|
4767
|
+
// search bounded to the far side of its own last item gives its remainder a full budget. Bounded
|
|
4768
|
+
// PER ALIAS and not once for the page, because aliases exhaust independently: one bound taken
|
|
4769
|
+
// from the merged page would sit below where an early-finishing alias stopped and re-serve
|
|
4770
|
+
// everything it had already delivered.
|
|
4771
|
+
let aliasSlide: string | undefined;
|
|
4772
|
+
// Capped AND out of pages is where the ceiling actually bites. Evaluated on its own rather than
|
|
4773
|
+
// inside the slide attempt below: a read that cannot slide at all (no order requested, so no
|
|
4774
|
+
// boundary to slide from) must still REPORT the ceiling, and folding the two together silently
|
|
4775
|
+
// dropped that for `searchMyIssues`, whose ordering is opt-in.
|
|
4776
|
+
const strandedHere = endCursor == null && issueCount > githubSearchResultLimit;
|
|
4777
|
+
if (strandedHere && sortKey != null) {
|
|
4778
|
+
const boundary = mapped != null ? lastSortBoundary(mapped, sortKey) : undefined;
|
|
4779
|
+
aliasSlide = boundary != null ? toGitHubIssueSearchSlideQualifier(sortKey, boundary) : undefined;
|
|
4780
|
+
// A bound identical to the one this alias is ALREADY under would re-issue the same query
|
|
4781
|
+
// forever. NOT needed for correctness, and that is measured rather than assumed: against a
|
|
4782
|
+
// provider that honours the bound there is no walk that reaches it. The nearest case, a tied
|
|
4783
|
+
// block larger than the cap, terminates on its own instead — every page inside the block is
|
|
4784
|
+
// filtered away by `slideSeen` until the walk ends on an empty page with no boundary to slide
|
|
4785
|
+
// from, so the second slide is never computed (verified: 1.200 issues sharing one second serve
|
|
4786
|
+
// the reachable 1.000 and report the ceiling, with this guard never firing).
|
|
4787
|
+
//
|
|
4788
|
+
// It is here purely as defence in depth against a bound that silently does nothing, because
|
|
4789
|
+
// that failure mode is an unbounded request loop rather than a wrong answer, and this read
|
|
4790
|
+
// walks until a provider tells it to stop. Cheap insurance against the one bug class that
|
|
4791
|
+
// cannot be shrugged off.
|
|
4792
|
+
if (aliasSlide != null && aliasSlide === cursorString(cursor, `slide:${s.alias}`)) {
|
|
4793
|
+
aliasSlide = undefined;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
if (strandedHere && aliasSlide == null) {
|
|
4797
|
+
strandedByCeiling = true;
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
if (aliasSlide != null) {
|
|
4801
|
+
// Restart this alias inside its new bound: a position from the old query is meaningless there.
|
|
4802
|
+
next[s.alias] = undefined;
|
|
4803
|
+
next[`slide:${s.alias}`] = aliasSlide;
|
|
4804
|
+
slid = true;
|
|
4805
|
+
} else {
|
|
4806
|
+
next[s.alias] = endCursor;
|
|
4807
|
+
// Carry the bound this alias is already walking under, or it would restart at the top.
|
|
4808
|
+
next[`slide:${s.alias}`] = cursorString(cursor, `slide:${s.alias}`);
|
|
4809
|
+
if (endCursor != null) {
|
|
4810
|
+
hasMore = true;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4484
4813
|
}
|
|
4485
4814
|
|
|
4486
|
-
//
|
|
4487
|
-
//
|
|
4488
|
-
//
|
|
4489
|
-
|
|
4490
|
-
|
|
4815
|
+
// Only the ceiling an alias could NOT slide past is incompleteness. A capped alias with pages left has
|
|
4816
|
+
// nothing to slide past yet, and one that slid has its remainder reachable through the cursor: marking
|
|
4817
|
+
// either would seal `truncated: true` and keep it there for the rest of the read, so a completed walk
|
|
4818
|
+
// would report results it went on to fetch as omitted.
|
|
4819
|
+
const truncated = cursor?.truncated === true || strandedByCeiling || continuationMissing;
|
|
4491
4820
|
next.truncated = truncated || undefined;
|
|
4821
|
+
const continuable = hasMore || slid;
|
|
4492
4822
|
return {
|
|
4493
4823
|
values: deduped,
|
|
4494
|
-
cursor:
|
|
4495
|
-
hasMore:
|
|
4824
|
+
cursor: continuable ? JSON.stringify(next) : undefined,
|
|
4825
|
+
hasMore: continuable,
|
|
4496
4826
|
page: page,
|
|
4497
4827
|
truncated: truncated,
|
|
4498
4828
|
totalCount: maxIssueCount,
|
|
@@ -4525,15 +4855,17 @@ export class GitHubApi {
|
|
|
4525
4855
|
avatarSize?: number;
|
|
4526
4856
|
cursor?: string;
|
|
4527
4857
|
pageSize?: number;
|
|
4858
|
+
/**
|
|
4859
|
+
* Uses the lightweight PR fragment, retaining identity, body, author, repository, branch refs, and stack
|
|
4860
|
+
* info while omitting review, check, and diff statistics. It also raises the default page budget, which
|
|
4861
|
+
* multi-facet searches share across their active facets.
|
|
4862
|
+
*/
|
|
4863
|
+
summary?: boolean;
|
|
4528
4864
|
},
|
|
4529
4865
|
cancellation?: AbortSignal,
|
|
4530
4866
|
): Promise<PullRequestSearchResult | undefined> {
|
|
4531
4867
|
const scope = getScopedLogger();
|
|
4532
4868
|
|
|
4533
|
-
const pageSize = Math.min(
|
|
4534
|
-
maxPullRequestSearchPageSize,
|
|
4535
|
-
Math.max(1, Math.trunc(options?.pageSize ?? defaultPullRequestSearchPageSize)),
|
|
4536
|
-
);
|
|
4537
4869
|
const facets = toGitHubPullRequestSearchFacets(options?.criteria);
|
|
4538
4870
|
const facetAliases = facets.map(f => f.alias).sort();
|
|
4539
4871
|
const scopeQualifiers = toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos);
|
|
@@ -4610,6 +4942,17 @@ export class GitHubApi {
|
|
|
4610
4942
|
};
|
|
4611
4943
|
}
|
|
4612
4944
|
|
|
4945
|
+
// Each active facet selects its own page in the same GraphQL document. Share the lite projection's
|
|
4946
|
+
// 100-node default across those selections; an explicit page size remains a per-facet request.
|
|
4947
|
+
const defaultSize =
|
|
4948
|
+
options?.summary === true
|
|
4949
|
+
? Math.max(1, Math.floor(maxPullRequestSearchPageSize / activeFacets.length))
|
|
4950
|
+
: defaultPullRequestSearchPageSize;
|
|
4951
|
+
const pageSize = Math.min(
|
|
4952
|
+
maxPullRequestSearchPageSize,
|
|
4953
|
+
Math.max(1, Math.trunc(options?.pageSize ?? defaultSize)),
|
|
4954
|
+
);
|
|
4955
|
+
|
|
4613
4956
|
const includeVar = (alias: string): string => `include${alias.charAt(0).toUpperCase()}${alias.slice(1)}`;
|
|
4614
4957
|
const params = facets.flatMap(f => [
|
|
4615
4958
|
`$${f.alias}Search: String!`,
|
|
@@ -4626,7 +4969,7 @@ export class GitHubApi {
|
|
|
4626
4969
|
}
|
|
4627
4970
|
nodes {
|
|
4628
4971
|
... on PullRequest {
|
|
4629
|
-
${gqlPullRequestFragment}
|
|
4972
|
+
${options?.summary ? gqlPullRequestLiteFragment : gqlPullRequestFragment}
|
|
4630
4973
|
${gqlPullRequestStackFragmentFor(options)}
|
|
4631
4974
|
}
|
|
4632
4975
|
}
|
|
@@ -4666,7 +5009,14 @@ export class GitHubApi {
|
|
|
4666
5009
|
if (node?.id == null) continue;
|
|
4667
5010
|
|
|
4668
5011
|
try {
|
|
4669
|
-
|
|
5012
|
+
// Must follow the projection: the lite fragment does not select reviews, checks or diff
|
|
5013
|
+
// stats, and the full mapper reads them — mapping a lite node with it would silently
|
|
5014
|
+
// report "no reviewers"/"no checks" as FACTS rather than as unselected.
|
|
5015
|
+
pullRequests.push(
|
|
5016
|
+
options?.summary
|
|
5017
|
+
? fromGitHubPullRequestLite(node, provider)
|
|
5018
|
+
: fromGitHubPullRequest(node, provider),
|
|
5019
|
+
);
|
|
4670
5020
|
} catch (ex) {
|
|
4671
5021
|
scope?.warn(`skipped unmappable pull request; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
4672
5022
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
IssueSearchCriteria,
|
|
3
|
+
IssueSearchRelationship,
|
|
4
|
+
IssueSortField,
|
|
5
|
+
IssueSorting,
|
|
6
|
+
} from '../../../git/models/issue.js';
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Translation of the provider-neutral issue-search criteria into GitHub search qualifiers, and the sanitizing
|
|
@@ -120,6 +125,49 @@ export function toGitHubIssueSortQualifier(sort: IssueSorting | undefined): stri
|
|
|
120
125
|
return sort != null ? gitHubIssueSortQualifiers[sort] : undefined;
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The sort keys a CEILING SLIDE can continue past, and the qualifier each one continues with.
|
|
130
|
+
*
|
|
131
|
+
* The slide works by re-issuing the search bounded to the far side of what has already been served, which needs
|
|
132
|
+
* two things from the sort key: a value carried on the returned issue (so the boundary can be read off the last
|
|
133
|
+
* item), and a GitHub range qualifier over that same value (so the continuation can be expressed). Only the two
|
|
134
|
+
* date keys have both. `comments`/`reactions` have range qualifiers but their values are unstable under
|
|
135
|
+
* concurrent activity — a comment added mid-walk moves an issue across the boundary, which would drop or repeat
|
|
136
|
+
* it — and `title` has no range qualifier at all. Those keep reporting the ceiling instead, which is correct
|
|
137
|
+
* rather than a gap: the read stays truthful about what it could not reach.
|
|
138
|
+
*
|
|
139
|
+
* A `Partial<Record<…>>` over the union rather than a lookup with a default, so a sort key added later is
|
|
140
|
+
* NOT-slidable by construction and has to be opted in deliberately.
|
|
141
|
+
*/
|
|
142
|
+
const gitHubSlidableSortFields: Partial<Record<IssueSortField, string>> = {
|
|
143
|
+
created: 'created',
|
|
144
|
+
updated: 'updated',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The qualifier that continues a search past the provider's result ceiling: everything at or beyond the last
|
|
149
|
+
* item already served, under the same key the search is ordered by.
|
|
150
|
+
*
|
|
151
|
+
* INCLUSIVE (`<=` / `>=`) rather than exclusive, and that is a correctness requirement rather than a preference.
|
|
152
|
+
* GitHub timestamps have one-second resolution and ties genuinely occur (measured: one shared second in a
|
|
153
|
+
* 100-issue sample), so an exclusive bound would silently drop every issue sharing the boundary second with the
|
|
154
|
+
* last one served. Being inclusive re-serves that second instead, which the caller dedupes.
|
|
155
|
+
*
|
|
156
|
+
* Returns `undefined` when the key can't be slid ({@link gitHubSlidableSortFields}), which is the signal to
|
|
157
|
+
* report the ceiling rather than to continue.
|
|
158
|
+
*/
|
|
159
|
+
export function toGitHubIssueSearchSlideQualifier(sort: IssueSorting, boundary: Date): string | undefined {
|
|
160
|
+
const [field, direction] = sort.split(':') as [IssueSortField, 'asc' | 'desc'];
|
|
161
|
+
const qualifier = gitHubSlidableSortFields[field];
|
|
162
|
+
if (qualifier == null) return undefined;
|
|
163
|
+
|
|
164
|
+
// Whole seconds, matching both GitHub's own resolution and what its range qualifiers accept.
|
|
165
|
+
const value = boundary.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
166
|
+
// Descending walks toward older, so the remainder is everything at or BEFORE the boundary; ascending is the
|
|
167
|
+
// mirror. Getting this backwards would re-request the half already served, forever.
|
|
168
|
+
return `${qualifier}:${direction === 'desc' ? '<=' : '>='}${value}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
123
171
|
/**
|
|
124
172
|
* The scope half of an issue search query: `org:` plus one `repo:` per repository.
|
|
125
173
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Cache } from '../../../../git/cache.js';
|
|
2
|
+
import { shouldEvictBlameCacheEntry } from '../../../../git/cache.js';
|
|
2
3
|
import type { GitBlame, GitBlameAuthor, ProgressiveGitBlame } from '../../../../git/models/blame.js';
|
|
3
4
|
import type { GitCommitLine } from '../../../../git/models/commit.js';
|
|
4
5
|
import { GitCommit, GitCommitIdentity } from '../../../../git/models/commit.js';
|
|
@@ -48,7 +49,7 @@ export class BlameGitHubSubProvider implements GitBlameSubProvider {
|
|
|
48
49
|
|
|
49
50
|
return createCompletedBlame(blame);
|
|
50
51
|
},
|
|
51
|
-
{ errorTTL: 1000 * 60 },
|
|
52
|
+
{ errorTTL: 1000 * 60, evictWhen: shouldEvictBlameCacheEntry },
|
|
52
53
|
);
|
|
53
54
|
return progressive?.completed;
|
|
54
55
|
}
|