@gitkraken/core-gitlens 0.5.103 → 0.5.104
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 -1
- package/dist/git/models/issue.d.ts +78 -0
- package/dist/git/models/issue.d.ts.map +1 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/plus/git-github/api/config.d.ts +12 -0
- package/dist/plus/git-github/api/config.d.ts.map +1 -1
- package/dist/plus/git-github/api/config.js +12 -1
- package/dist/plus/git-github/api/config.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +77 -7
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +241 -120
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/api/issueSearchQuery.d.ts +70 -0
- package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -0
- package/dist/plus/git-github/api/issueSearchQuery.js +143 -0
- package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +2 -0
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +39 -2
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +17 -0
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +88 -2
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +61 -3
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +51 -0
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +3 -46
- 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/models/issueReads.d.ts +70 -0
- package/dist/plus/integrations/models/issueReads.d.ts.map +1 -0
- package/dist/plus/integrations/models/issueReads.js +2 -0
- package/dist/plus/integrations/models/issueReads.js.map +1 -0
- package/dist/plus/integrations/providerFilters.d.ts +10 -0
- package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +26 -3
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +34 -0
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +22 -0
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +26 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts +10 -0
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +64 -0
- package/dist/plus/integrations/reads/counts.d.ts.map +1 -0
- package/dist/plus/integrations/reads/counts.js +164 -0
- package/dist/plus/integrations/reads/counts.js.map +1 -0
- package/dist/plus/integrations/reads/filters.d.ts +63 -1
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +125 -0
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js +22 -41
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/paging.d.ts +59 -0
- package/dist/plus/integrations/reads/paging.d.ts.map +1 -1
- package/dist/plus/integrations/reads/paging.js +60 -0
- package/dist/plus/integrations/reads/paging.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.d.ts +55 -0
- package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -0
- package/dist/plus/integrations/reads/searchIssues.js +144 -0
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -0
- package/dist/plus/integrations/reads/warnings.d.ts +25 -1
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +86 -2
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/docs/integrations.md +110 -2
- package/docs/kepler-read-api-parity.md +57 -0
- package/package.json +1 -1
- package/src/git/models/issue.ts +81 -0
- package/src/plus/git-github/api/config.ts +13 -0
- package/src/plus/git-github/api/github.ts +332 -148
- package/src/plus/git-github/api/issueSearchQuery.ts +165 -0
- package/src/plus/integrations/index.ts +5 -0
- package/src/plus/integrations/integrationService.ts +47 -1
- package/src/plus/integrations/manager.ts +88 -2
- package/src/plus/integrations/models/gitHostIntegration.ts +102 -2
- package/src/plus/integrations/models/integration.ts +7 -46
- package/src/plus/integrations/models/issueReads.ts +73 -0
- package/src/plus/integrations/providerFilters.ts +15 -0
- package/src/plus/integrations/providers/github.ts +64 -2
- package/src/plus/integrations/providers/models.ts +49 -0
- package/src/plus/integrations/reads/broaden.ts +10 -0
- package/src/plus/integrations/reads/counts.ts +285 -0
- package/src/plus/integrations/reads/filters.ts +162 -1
- package/src/plus/integrations/reads/issues.ts +22 -45
- package/src/plus/integrations/reads/paging.ts +102 -0
- package/src/plus/integrations/reads/searchIssues.ts +252 -0
- package/src/plus/integrations/reads/warnings.ts +114 -4
|
@@ -23,6 +23,8 @@ import { maybeStopWatch } from '../../../utils/stopwatch.js';
|
|
|
23
23
|
import { parseUri } from '../../../utils/uri.js';
|
|
24
24
|
import { fromString, satisfies } from '../../../utils/version.js';
|
|
25
25
|
import { fromGitHubIssue, fromGitHubIssueOrPullRequestState, fromGitHubPullRequest, fromGitHubPullRequestLite, } from '../models.js';
|
|
26
|
+
import { githubSearchResultLimit } from './config.js';
|
|
27
|
+
import { gitHubIssueSearchRelationships, toGitHubIssueSearchQualifiers, toGitHubIssueSearchScopeQualifiers, } from './issueSearchQuery.js';
|
|
26
28
|
const emptyPagedResult = Object.freeze({ values: [] });
|
|
27
29
|
const emptyBlameResult = Object.freeze({ ranges: [] });
|
|
28
30
|
// Transient gateway/network failures (e.g. an upstream `502 Bad Gateway`) are worth a few quick
|
|
@@ -2203,7 +2205,7 @@ export class GitHubApi {
|
|
|
2203
2205
|
values: results,
|
|
2204
2206
|
cursor: rsp.search.pageInfo.endCursor ?? undefined,
|
|
2205
2207
|
hasMore: rsp.search.pageInfo.hasNextPage,
|
|
2206
|
-
truncated: rsp.search.issueCount >
|
|
2208
|
+
truncated: rsp.search.issueCount > githubSearchResultLimit,
|
|
2207
2209
|
};
|
|
2208
2210
|
}
|
|
2209
2211
|
catch (ex) {
|
|
@@ -2214,7 +2216,164 @@ export class GitHubApi {
|
|
|
2214
2216
|
return (await this.searchMyPullRequestsPage(provider, token, options, cancellation)).values;
|
|
2215
2217
|
}
|
|
2216
2218
|
async searchMyIssues(provider, token, options, cancellation) {
|
|
2219
|
+
let search = options?.search?.trim() ?? '';
|
|
2220
|
+
if (options?.user) {
|
|
2221
|
+
search += ` user:${options.user}`;
|
|
2222
|
+
}
|
|
2223
|
+
if (options?.repos != null && options.repos.length > 0) {
|
|
2224
|
+
const repo = ' repo:';
|
|
2225
|
+
search += `${repo}${options.repos.join(repo)}`;
|
|
2226
|
+
}
|
|
2227
|
+
const baseFilters = 'type:issue is:open archived:false';
|
|
2228
|
+
// `includeAllAssignees` broadens the assigned category from "assigned to me" to "assigned to anyone"
|
|
2229
|
+
// (`assignee:*` is GitHub's has-any-assignee qualifier). Authored/mentioned stay bound to `@me` — they're
|
|
2230
|
+
// user-relative by definition, so an all-assignees read still only surfaces the current user's authored
|
|
2231
|
+
// and mentioned issues plus every assigned-to-anyone issue.
|
|
2232
|
+
//
|
|
2233
|
+
// NOTE: `assignee:*` requires a SCOPE to be meaningful, but any scope will do — one repository, several,
|
|
2234
|
+
// or an org (measured: `repo:a repo:b … assignee:*` returns exactly the sum of the two per-repo counts).
|
|
2235
|
+
// It is only the UNSCOPED form that is meaningless, matching millions of issues across all of GitHub, so
|
|
2236
|
+
// callers must supply `repos` (or an org qualifier via `search`); the facade refuses the unscoped case.
|
|
2237
|
+
const assignedQualifier = options?.includeAllAssignees ? 'assignee:*' : 'assignee:@me';
|
|
2238
|
+
// A category is read when the caller asked for it AND it hasn't been exhausted (a `null` cursor slot,
|
|
2239
|
+
// which `searchIssuesByAlias` maintains). An excluded category needs no cursor bookkeeping: a missing
|
|
2240
|
+
// response category reads back as `null`, so it stays excluded across continuations on its own.
|
|
2241
|
+
const categories = options?.categories;
|
|
2242
|
+
const requested = {
|
|
2243
|
+
authored: categories?.authored ?? categories == null,
|
|
2244
|
+
assigned: categories?.assigned ?? categories == null,
|
|
2245
|
+
mentioned: categories?.mentioned ?? categories == null,
|
|
2246
|
+
};
|
|
2247
|
+
// Two things are fixed here rather than incidental:
|
|
2248
|
+
// - the alias names are this read's persisted cursor keys, so they can't be renamed;
|
|
2249
|
+
// - the ORDER is assigned → mentioned → authored, which is the order the union is emitted in and, because
|
|
2250
|
+
// the dedupe keeps the first occurrence of a url, also the precedence between the three. An issue that
|
|
2251
|
+
// is both assigned to and authored by the user surfaces as the assigned one.
|
|
2252
|
+
const searches = [];
|
|
2253
|
+
if (requested.assigned) {
|
|
2254
|
+
searches.push({ alias: 'assigned', query: `${search} ${baseFilters} ${assignedQualifier}`.trim() });
|
|
2255
|
+
}
|
|
2256
|
+
if (requested.mentioned) {
|
|
2257
|
+
searches.push({ alias: 'mentioned', query: `${search} ${baseFilters} mentions:@me`.trim() });
|
|
2258
|
+
}
|
|
2259
|
+
if (requested.authored) {
|
|
2260
|
+
searches.push({ alias: 'authored', query: `${search} ${baseFilters} author:@me`.trim() });
|
|
2261
|
+
}
|
|
2262
|
+
return this.searchIssuesByAlias(provider, token, searches, options, cancellation);
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* The filtered issue search: issues matching structured criteria over a repository/org scope, with no forced
|
|
2266
|
+
* relationship to the current user. The issue counterpart of {@link searchMyPullRequestsPage}, and distinct
|
|
2267
|
+
* from {@link searchMyIssues}, which is permanently bound to `@me`.
|
|
2268
|
+
*
|
|
2269
|
+
* Ordering is part of the contract, not an option: always `sort:updated` (most recently updated first). A
|
|
2270
|
+
* consumer's "show the N most recent" policy at GitHub's result ceiling is only correct under a guaranteed
|
|
2271
|
+
* order, and an option would let a caller pick relevance order and then truncate to an arbitrary subset.
|
|
2272
|
+
*
|
|
2273
|
+
* Each requested relationship becomes its own aliased search, unioned and deduped by url; with none, a single
|
|
2274
|
+
* search runs over the scope alone. `criteria.text` and the other free-form values are sanitized so user input
|
|
2275
|
+
* cannot inject a qualifier and re-scope the search — see {@link toGitHubIssueSearchQualifiers}.
|
|
2276
|
+
*/
|
|
2277
|
+
async searchIssuesPage(provider, token, options, cancellation) {
|
|
2278
|
+
const base = [
|
|
2279
|
+
...toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos),
|
|
2280
|
+
...toGitHubIssueSearchQualifiers(options?.criteria),
|
|
2281
|
+
].join(' ');
|
|
2282
|
+
// One aliased search per relationship, OR-ed by union. They can't be one query: GitHub AND-s qualifiers,
|
|
2283
|
+
// so `author:@me assignee:@me` would return the intersection — issues the user both opened and is assigned
|
|
2284
|
+
// to — instead of either set. With no relationship the scope + criteria are already the whole query.
|
|
2285
|
+
const relationships = options?.criteria?.relationships;
|
|
2286
|
+
const searches = relationships?.length
|
|
2287
|
+
? relationships.map(r => ({
|
|
2288
|
+
alias: gitHubIssueSearchRelationships[r].alias,
|
|
2289
|
+
query: `${base} ${gitHubIssueSearchRelationships[r].qualifier}`.trim(),
|
|
2290
|
+
}))
|
|
2291
|
+
: [{ alias: 'matched', query: base }];
|
|
2292
|
+
return this.searchIssuesByAlias(provider, token, searches, options, cancellation);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Counts issues for several scopes in ONE request, transferring no issues at all — each scope is an aliased
|
|
2296
|
+
* `search` selecting only `issueCount`, with `first: 0` so no nodes are fetched.
|
|
2297
|
+
*
|
|
2298
|
+
* This is what makes a "this will fetch ~N issues" preview affordable: measured against the live API, 30
|
|
2299
|
+
* aliased counts cost a single rate-limit point. It is still a network request per chunk, so a caller is
|
|
2300
|
+
* expected to debounce and cache.
|
|
2301
|
+
*
|
|
2302
|
+
* Returns counts positionally — one per input scope, same order — because a caller-supplied key must never
|
|
2303
|
+
* reach the GraphQL document (it would break it); the aliases are generated. `undefined` in a slot means the
|
|
2304
|
+
* response omitted that alias, which the caller reports as "not counted" rather than as zero.
|
|
2305
|
+
*/
|
|
2306
|
+
async countIssues(provider, token, scopes, options, cancellation) {
|
|
2307
|
+
const scope = getScopedLogger();
|
|
2308
|
+
if (scopes.length === 0)
|
|
2309
|
+
return [];
|
|
2310
|
+
const queries = scopes.map(s => {
|
|
2311
|
+
const qualifiers = [
|
|
2312
|
+
...toGitHubIssueSearchScopeQualifiers(s.org, s.repos),
|
|
2313
|
+
...toGitHubIssueSearchQualifiers(s.criteria),
|
|
2314
|
+
];
|
|
2315
|
+
// A relationship set is OR-ed across searches, which a single count can't express — the facade splits
|
|
2316
|
+
// such a scope into one count per relationship before calling, so at most one is present here.
|
|
2317
|
+
const relationship = s.criteria?.relationships?.[0];
|
|
2318
|
+
if (relationship != null) {
|
|
2319
|
+
qualifiers.push(gitHubIssueSearchRelationships[relationship].qualifier);
|
|
2320
|
+
}
|
|
2321
|
+
return qualifiers.join(' ');
|
|
2322
|
+
});
|
|
2323
|
+
// Aliases are positional and generated (`s0`, `s1`, …): a caller's key is arbitrary text and would break
|
|
2324
|
+
// the document, so the caller maps results back by index.
|
|
2325
|
+
const params = queries.map((_, i) => `$q${i}: String!`).join('\n\t\t\t\t');
|
|
2326
|
+
// `first: 0` is what makes this cheap — `issueCount` alone, no nodes over the wire.
|
|
2327
|
+
const fields = queries
|
|
2328
|
+
.map((_, i) => `s${i}: search(query: $q${i}, type: ISSUE, first: 0) { issueCount }`)
|
|
2329
|
+
.join('\n\t\t\t\t');
|
|
2330
|
+
const query = `query countIssues(
|
|
2331
|
+
${params}
|
|
2332
|
+
) {
|
|
2333
|
+
${fields}
|
|
2334
|
+
}`;
|
|
2335
|
+
const variables = { baseUrl: options?.baseUrl };
|
|
2336
|
+
queries.forEach((q, i) => {
|
|
2337
|
+
variables[`q${i}`] = q;
|
|
2338
|
+
});
|
|
2339
|
+
try {
|
|
2340
|
+
const rsp = await this.graphql(provider, token, query, variables, scope, cancellation);
|
|
2341
|
+
if (rsp == null)
|
|
2342
|
+
return queries.map(() => undefined);
|
|
2343
|
+
return queries.map((_, i) => rsp[`s${i}`]?.issueCount);
|
|
2344
|
+
}
|
|
2345
|
+
catch (ex) {
|
|
2346
|
+
throw this.handleException(ex, provider, scope);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* The aliased-search engine behind every GitHub issue search: one GraphQL request carrying N independently
|
|
2351
|
+
* cursored `search` fields, `@include`-gated so an exhausted or unrequested one costs nothing.
|
|
2352
|
+
*
|
|
2353
|
+
* It exists as one implementation because the properties that make it correct are subtle and must not be
|
|
2354
|
+
* reproduced per read: each alias advances on its OWN cursor and is dropped from the request once exhausted
|
|
2355
|
+
* (so a finished search is never re-queried, which would re-emit its first page); results are mapped
|
|
2356
|
+
* node-by-node so one unmappable issue can't discard the page; the union is deduped by `url` rather than by
|
|
2357
|
+
* `IssueShape.id`, which for some providers is a per-repository number; and a provider that claims another
|
|
2358
|
+
* page while withholding its `endCursor` is reported as truncated instead of paged forever.
|
|
2359
|
+
*
|
|
2360
|
+
* {@link searchMyIssues} is one configuration of it (its three `@me` categories), and its alias names are
|
|
2361
|
+
* that read's published cursor keys.
|
|
2362
|
+
*
|
|
2363
|
+
* `searches` must have unique aliases, each a valid GraphQL name that is neither `page` nor `truncated` —
|
|
2364
|
+
* the composite cursor keys aliases at its top level, alongside those two reserved fields.
|
|
2365
|
+
*/
|
|
2366
|
+
async searchIssuesByAlias(provider, token, searches, options, cancellation) {
|
|
2217
2367
|
const scope = getScopedLogger();
|
|
2368
|
+
// Enforced, not just documented: aliases share the cursor's top level with `page` and `truncated`, so an
|
|
2369
|
+
// alias colliding with either would overwrite it — and the failure would be SILENT, a page number replaced
|
|
2370
|
+
// by a cursor string that reads back as page 1, restarting the walk with no error and no truncation flag.
|
|
2371
|
+
// Cheap to check, and it fails at the one call that introduced the collision rather than in a consumer's
|
|
2372
|
+
// persisted cursor.
|
|
2373
|
+
const reserved = searches.filter(s => s.alias === 'page' || s.alias === 'truncated');
|
|
2374
|
+
if (reserved.length > 0) {
|
|
2375
|
+
throw new Error(`Issue search alias(es) ${reserved.map(s => `'${s.alias}'`).join(', ')} collide with the composite cursor's reserved keys`);
|
|
2376
|
+
}
|
|
2218
2377
|
let cursor;
|
|
2219
2378
|
if (options?.cursor != null) {
|
|
2220
2379
|
try {
|
|
@@ -2223,37 +2382,34 @@ export class GitHubApi {
|
|
|
2223
2382
|
catch { }
|
|
2224
2383
|
}
|
|
2225
2384
|
const page = Math.max(1, Math.trunc(cursor?.page ?? 1));
|
|
2226
|
-
// A
|
|
2227
|
-
//
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
authored: categories?.authored ?? categories == null,
|
|
2232
|
-
assigned: categories?.assigned ?? categories == null,
|
|
2233
|
-
mentioned: categories?.mentioned ?? categories == null,
|
|
2385
|
+
// A slot is a continuation string, `null` (exhausted), or absent. Anything else came from a malformed or
|
|
2386
|
+
// foreign cursor, and is read as absent rather than threaded back into the request as a continuation.
|
|
2387
|
+
const slotFor = (alias) => {
|
|
2388
|
+
const slot = cursor?.[alias];
|
|
2389
|
+
return slot === null || typeof slot === 'string' ? slot : undefined;
|
|
2234
2390
|
};
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2391
|
+
// Every requested search is in the document; an exhausted one is switched OFF by its `@include` gate
|
|
2392
|
+
// rather than removed, so a continuation's request keeps the shape of the first one and GitHub's
|
|
2393
|
+
// query-document cache still recognizes it. `active` is what actually runs.
|
|
2394
|
+
const isActive = (s) => slotFor(s.alias) !== null;
|
|
2395
|
+
const active = searches.filter(isActive);
|
|
2396
|
+
// Reading nothing is the honest answer to "every requested search is exhausted (or none was requested)":
|
|
2397
|
+
// widening back to the full set would return items the caller excluded, or re-emit a finished page.
|
|
2398
|
+
if (active.length === 0) {
|
|
2241
2399
|
return { values: [], hasMore: false, page: page, truncated: cursor?.truncated === true };
|
|
2242
2400
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
authored: search(first: 100, after: $authoredCursor, query: $authored, type: ISSUE)
|
|
2256
|
-
@include(if: $includeAuthored) {
|
|
2401
|
+
// GitHub's search caps a page at 100 regardless of what is asked for.
|
|
2402
|
+
const pageSize = Math.min(100, Math.max(1, Math.trunc(options?.pageSize ?? 100)));
|
|
2403
|
+
// `includeAssigned` etc. — the same variable names the read published before the aliases were made
|
|
2404
|
+
// data-driven, so a recorded/asserted request shape stays recognizable.
|
|
2405
|
+
const includeVar = (alias) => `include${alias.charAt(0).toUpperCase()}${alias.slice(1)}`;
|
|
2406
|
+
const params = searches.flatMap(s => [
|
|
2407
|
+
`$${s.alias}: String!`,
|
|
2408
|
+
`$${s.alias}Cursor: String`,
|
|
2409
|
+
`$${includeVar(s.alias)}: Boolean!`,
|
|
2410
|
+
]);
|
|
2411
|
+
const fields = searches.map(s => `${s.alias}: search(first: ${pageSize}, after: $${s.alias}Cursor, query: $${s.alias}, type: ISSUE)
|
|
2412
|
+
@include(if: $${includeVar(s.alias)}) {
|
|
2257
2413
|
issueCount
|
|
2258
2414
|
pageInfo {
|
|
2259
2415
|
endCursor
|
|
@@ -2265,108 +2421,66 @@ export class GitHubApi {
|
|
|
2265
2421
|
${options?.includeBody ? 'body' : ''}
|
|
2266
2422
|
}
|
|
2267
2423
|
}
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
hasNextPage
|
|
2275
|
-
}
|
|
2276
|
-
nodes {
|
|
2277
|
-
... on Issue {
|
|
2278
|
-
${gqIssueFragment}
|
|
2279
|
-
${options?.includeBody ? 'body' : ''}
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
mentioned: search(first: 100, after: $mentionedCursor, query: $mentioned, type: ISSUE)
|
|
2284
|
-
@include(if: $includeMentioned) {
|
|
2285
|
-
issueCount
|
|
2286
|
-
pageInfo {
|
|
2287
|
-
endCursor
|
|
2288
|
-
hasNextPage
|
|
2289
|
-
}
|
|
2290
|
-
nodes {
|
|
2291
|
-
... on Issue {
|
|
2292
|
-
${gqIssueFragment}
|
|
2293
|
-
${options?.includeBody ? 'body' : ''}
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2424
|
+
}`);
|
|
2425
|
+
const query = `query searchIssues(
|
|
2426
|
+
${params.join('\n\t\t\t\t')}
|
|
2427
|
+
$avatarSize: Int
|
|
2428
|
+
) {
|
|
2429
|
+
${fields.join('\n\t\t\t\t')}
|
|
2297
2430
|
}`;
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2431
|
+
const variables = {
|
|
2432
|
+
baseUrl: options?.baseUrl,
|
|
2433
|
+
avatarSize: options?.avatarSize,
|
|
2434
|
+
};
|
|
2435
|
+
for (const s of searches) {
|
|
2436
|
+
variables[s.alias] = s.query;
|
|
2437
|
+
variables[`${s.alias}Cursor`] = slotFor(s.alias) ?? undefined;
|
|
2438
|
+
variables[includeVar(s.alias)] = isActive(s);
|
|
2305
2439
|
}
|
|
2306
|
-
const baseFilters = 'type:issue is:open archived:false';
|
|
2307
|
-
// `includeAllAssignees` broadens the assigned category from "assigned to me" to "assigned to anyone"
|
|
2308
|
-
// (`assignee:*` is GitHub's has-any-assignee qualifier). Authored/mentioned stay bound to `@me` — they're
|
|
2309
|
-
// user-relative by definition, so an all-assignees read still only surfaces the current user's authored
|
|
2310
|
-
// and mentioned issues plus every assigned-to-anyone issue.
|
|
2311
|
-
//
|
|
2312
|
-
// NOTE: GitHub only honors `assignee:*` when the search is scoped to a single repository (see GitHub's
|
|
2313
|
-
// issue-search docs). Callers using `includeAllAssignees` must scope the read via `repos` (the broaden
|
|
2314
|
-
// fan-out does this per org/repo); an unscoped account-wide `assignee:*` is not meaningful to GitHub.
|
|
2315
|
-
const assignedQualifier = options?.includeAllAssignees ? 'assignee:*' : 'assignee:@me';
|
|
2316
2440
|
try {
|
|
2317
|
-
const rsp = await this.graphql(provider, token, query,
|
|
2318
|
-
authored: `${search} ${baseFilters} author:@me`.trim(),
|
|
2319
|
-
assigned: `${search} ${baseFilters} ${assignedQualifier}`.trim(),
|
|
2320
|
-
mentioned: `${search} ${baseFilters} mentions:@me`.trim(),
|
|
2321
|
-
authoredCursor: cursor?.authored ?? undefined,
|
|
2322
|
-
assignedCursor: cursor?.assigned ?? undefined,
|
|
2323
|
-
mentionedCursor: cursor?.mentioned ?? undefined,
|
|
2324
|
-
includeAuthored: includeAuthored,
|
|
2325
|
-
includeAssigned: includeAssigned,
|
|
2326
|
-
includeMentioned: includeMentioned,
|
|
2327
|
-
baseUrl: options?.baseUrl,
|
|
2328
|
-
avatarSize: options?.avatarSize,
|
|
2329
|
-
}, scope, cancellation);
|
|
2441
|
+
const rsp = await this.graphql(provider, token, query, variables, scope, cancellation);
|
|
2330
2442
|
if (rsp == null)
|
|
2331
2443
|
return { values: [], hasMore: false, page: page, truncated: false };
|
|
2332
2444
|
// Map node-by-node so one unmappable issue can't discard the whole result set
|
|
2333
2445
|
const issues = [];
|
|
2334
|
-
for (const
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
catch (ex) {
|
|
2345
|
-
scope?.warn(`skipped unmappable issue; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
2446
|
+
for (const s of active) {
|
|
2447
|
+
for (const node of rsp[s.alias]?.nodes ?? []) {
|
|
2448
|
+
if (node?.id == null)
|
|
2449
|
+
continue;
|
|
2450
|
+
try {
|
|
2451
|
+
issues.push(fromGitHubIssue(node, provider));
|
|
2452
|
+
}
|
|
2453
|
+
catch (ex) {
|
|
2454
|
+
scope?.warn(`skipped unmappable issue; id=${node.id}, url=${node.url}, ex=${ex}`);
|
|
2455
|
+
}
|
|
2346
2456
|
}
|
|
2347
2457
|
}
|
|
2458
|
+
// Dedupe by `url`, not `IssueShape.id`: for some providers `id` is a per-repository number, so an
|
|
2459
|
+
// id-keyed map would collapse distinct issues across repositories.
|
|
2348
2460
|
const results = uniqueBy(issues, r => r.url, (original, _current) => original);
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2461
|
+
// Every alias gets a slot, so an inactive one keeps its `null` and stays out of the next request. A
|
|
2462
|
+
// missing slot would be read as "never requested", which for a `searches` set that still lists it
|
|
2463
|
+
// would restart it from its first page.
|
|
2464
|
+
const next = { page: page + 1 };
|
|
2465
|
+
let hasMore = false;
|
|
2466
|
+
let continuationMissing = false;
|
|
2467
|
+
let maxIssueCount = 0;
|
|
2468
|
+
for (const s of searches) {
|
|
2469
|
+
const category = rsp[s.alias];
|
|
2470
|
+
const endCursor = category?.pageInfo?.hasNextPage && category.pageInfo.endCursor ? category.pageInfo.endCursor : null;
|
|
2471
|
+
next[s.alias] = endCursor;
|
|
2472
|
+
if (endCursor != null) {
|
|
2473
|
+
hasMore = true;
|
|
2352
2474
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
const
|
|
2362
|
-
// GitHub search exposes at most 1,000 results per category. Paging removes the previous 100-item
|
|
2363
|
-
// truncation; only the upstream search ceiling or an unusable continuation remains incomplete.
|
|
2364
|
-
const continuationMissing = [rsp.authored, rsp.assigned, rsp.mentioned].some(category => category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null);
|
|
2365
|
-
const truncated = cursor?.truncated === true ||
|
|
2366
|
-
(rsp.authored?.issueCount ?? 0) > 1000 ||
|
|
2367
|
-
(rsp.assigned?.issueCount ?? 0) > 1000 ||
|
|
2368
|
-
(rsp.mentioned?.issueCount ?? 0) > 1000 ||
|
|
2369
|
-
continuationMissing;
|
|
2475
|
+
if (category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null) {
|
|
2476
|
+
continuationMissing = true;
|
|
2477
|
+
}
|
|
2478
|
+
maxIssueCount = Math.max(maxIssueCount, category?.issueCount ?? 0);
|
|
2479
|
+
}
|
|
2480
|
+
// GitHub search exposes at most `githubSearchResultLimit` results PER SEARCH, so the ceiling is
|
|
2481
|
+
// reached as soon as any one alias exceeds it. Paging removes the old 100-item truncation; only that
|
|
2482
|
+
// upstream ceiling or an unusable continuation leaves the read incomplete.
|
|
2483
|
+
const truncated = cursor?.truncated === true || maxIssueCount > githubSearchResultLimit || continuationMissing;
|
|
2370
2484
|
next.truncated = truncated || undefined;
|
|
2371
2485
|
return {
|
|
2372
2486
|
values: [...results],
|
|
@@ -2374,6 +2488,7 @@ export class GitHubApi {
|
|
|
2374
2488
|
hasMore: hasMore,
|
|
2375
2489
|
page: page,
|
|
2376
2490
|
truncated: truncated,
|
|
2491
|
+
totalCount: maxIssueCount,
|
|
2377
2492
|
};
|
|
2378
2493
|
}
|
|
2379
2494
|
catch (ex) {
|
|
@@ -2772,6 +2887,12 @@ __decorate([
|
|
|
2772
2887
|
__decorate([
|
|
2773
2888
|
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
2774
2889
|
], GitHubApi.prototype, "searchMyIssues", null);
|
|
2890
|
+
__decorate([
|
|
2891
|
+
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
2892
|
+
], GitHubApi.prototype, "searchIssuesPage", null);
|
|
2893
|
+
__decorate([
|
|
2894
|
+
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
2895
|
+
], GitHubApi.prototype, "countIssues", null);
|
|
2775
2896
|
__decorate([
|
|
2776
2897
|
trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
|
|
2777
2898
|
], GitHubApi.prototype, "searchPullRequests", null);
|