Skip to content

Commit 6cb4ac0

Browse files
committed
Adds better error/results for pr search queries
Adds experimental flags for pr search queries
1 parent 431fbed commit 6cb4ac0

File tree

7 files changed

+189
-21
lines changed

7 files changed

+189
-21
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3060,6 +3060,20 @@
30603060
"markdownDescription": "Specifies whether to allow opening multiple instances of the _Launchpad_ as an editor tab",
30613061
"scope": "window",
30623062
"order": 1000
3063+
},
3064+
"gitlens.launchpad.experimental.queryLimit": {
3065+
"type": "number",
3066+
"default": 100,
3067+
"markdownDescription": "Specifies an experimental limit on the number of pull requests to be queried in the _Launchpad_",
3068+
"scope": "window",
3069+
"order": 1100
3070+
},
3071+
"gitlens.launchpad.experimental.queryUseInvolvesFilter": {
3072+
"type": "boolean",
3073+
"default": false,
3074+
"markdownDescription": "Specifies an experimental flag whether to use the `involves` filter in the GraphQL query for _Launchpad_",
3075+
"scope": "window",
3076+
"order": 1110
30633077
}
30643078
}
30653079
},

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ export interface Config {
102102
interval: number;
103103
};
104104
};
105+
readonly experimental: {
106+
readonly queryLimit: number;
107+
readonly queryUseInvolvesFilter: boolean;
108+
};
105109
};
106110
readonly gitCommands: {
107111
readonly avatars: boolean;

src/plus/focus/focusProvider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { UriTypes } from '../../uris/deepLinks/deepLink';
2525
import { DeepLinkActionType, DeepLinkType } from '../../uris/deepLinks/deepLink';
2626
import { showInspectView } from '../../webviews/commitDetails/actions';
2727
import type { ShowWipArgs } from '../../webviews/commitDetails/protocol';
28+
import type { IntegrationResult } from '../integrations/integration';
2829
import type { EnrichablePullRequest, ProviderActionablePullRequest } from '../integrations/providers/models';
2930
import {
3031
getActionablePullRequests,
@@ -158,7 +159,7 @@ type CachedFocusPromise<T> = {
158159
const cacheExpiration = 1000 * 60 * 30; // 30 minutes
159160

160161
type PullRequestsWithSuggestionCounts = {
161-
prs: TimedResult<SearchedPullRequest[] | undefined> | undefined;
162+
prs: IntegrationResult<SearchedPullRequest[] | undefined> | undefined;
162163
suggestionCounts: TimedResult<CodeSuggestionCounts | undefined> | undefined;
163164
};
164165

@@ -250,7 +251,12 @@ export class FocusProvider implements Disposable {
250251
throw prsResult.reason;
251252
}
252253

253-
const prs = getSettledValue(prsResult);
254+
const prs = getSettledValue(prsResult)?.value;
255+
if (prs?.error != null) {
256+
Logger.error(prs.error, scope, 'Failed to get pull requests');
257+
throw prs.error;
258+
}
259+
254260
const subscription = getSettledValue(subscriptionResult);
255261

256262
let suggestionCounts;

src/plus/integrations/integration.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ import type {
4444
import { HostingIntegrationId, IssueFilter, PagingMode, PullRequestFilter } from './providers/models';
4545
import type { ProvidersApi } from './providers/providersApi';
4646

47+
export type IntegrationResult<T> =
48+
| { value: T; duration?: number; error?: never }
49+
| { error: Error; duration?: number; value?: never }
50+
| undefined;
51+
4752
export type SupportedIntegrationIds = IntegrationId;
4853
export type SupportedHostingIntegrationIds = HostingIntegrationId;
4954
export type SupportedIssueIntegrationIds = IssueIntegrationId;
@@ -1129,30 +1134,34 @@ export abstract class HostingIntegration<
11291134
}
11301135
}
11311136

1132-
async searchMyPullRequests(repo?: T, cancellation?: CancellationToken): Promise<SearchedPullRequest[] | undefined>;
1137+
async searchMyPullRequests(
1138+
repo?: T,
1139+
cancellation?: CancellationToken,
1140+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>>;
11331141
async searchMyPullRequests(
11341142
repos?: T[],
11351143
cancellation?: CancellationToken,
1136-
): Promise<SearchedPullRequest[] | undefined>;
1144+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>>;
11371145
@debug()
11381146
async searchMyPullRequests(
11391147
repos?: T | T[],
11401148
cancellation?: CancellationToken,
1141-
): Promise<SearchedPullRequest[] | undefined> {
1149+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>> {
11421150
const scope = getLogScope();
11431151
const connected = this.maybeConnected ?? (await this.isConnected());
11441152
if (!connected) return undefined;
11451153

1154+
const start = Date.now();
11461155
try {
11471156
const pullRequests = await this.searchProviderMyPullRequests(
11481157
this._session!,
11491158
repos != null ? (Array.isArray(repos) ? repos : [repos]) : undefined,
11501159
cancellation,
11511160
);
1152-
this.resetRequestExceptionCount();
1153-
return pullRequests;
1161+
return { value: pullRequests, duration: Date.now() - start };
11541162
} catch (ex) {
1155-
return this.handleProviderException<SearchedPullRequest[] | undefined>(ex, scope, undefined);
1163+
Logger.error(ex, scope);
1164+
return { error: ex, duration: Date.now() - start };
11561165
}
11571166
}
11581167

src/plus/integrations/integrationService.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
Integration,
2020
IntegrationBase,
2121
IntegrationKey,
22+
IntegrationResult,
2223
IntegrationType,
2324
IssueIntegration,
2425
ResourceDescriptor,
@@ -409,7 +410,7 @@ export class IntegrationService implements Disposable {
409410
async getMyPullRequests(
410411
integrationIds?: HostingIntegrationId[],
411412
cancellation?: CancellationToken,
412-
): Promise<SearchedPullRequest[] | undefined> {
413+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>> {
413414
const integrations: Map<HostingIntegration, ResourceDescriptor[] | undefined> = new Map();
414415
for (const integrationId of integrationIds?.length ? integrationIds : Object.values(HostingIntegrationId)) {
415416
const integration = await this.get(integrationId);
@@ -425,26 +426,54 @@ export class IntegrationService implements Disposable {
425426
private async getMyPullRequestsCore(
426427
integrations: Map<HostingIntegration, ResourceDescriptor[] | undefined>,
427428
cancellation?: CancellationToken,
428-
): Promise<SearchedPullRequest[] | undefined> {
429-
const promises: Promise<SearchedPullRequest[] | undefined>[] = [];
429+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>> {
430+
const start = Date.now();
431+
432+
const promises: Promise<IntegrationResult<SearchedPullRequest[] | undefined>>[] = [];
430433
for (const [integration, repos] of integrations) {
431434
if (integration == null) continue;
432435

433436
promises.push(integration.searchMyPullRequests(repos, cancellation));
434437
}
435438

436439
const results = await Promise.allSettled(promises);
437-
return [...flatten(filterMap(results, r => (r.status === 'fulfilled' ? r.value : undefined)))];
440+
441+
const errors = [
442+
...filterMap(results, r =>
443+
r.status === 'fulfilled' && r.value?.error != null ? r.value.error : undefined,
444+
),
445+
];
446+
if (errors.length) {
447+
return {
448+
error: errors.length === 1 ? errors[0] : new AggregateError(errors),
449+
duration: Date.now() - start,
450+
};
451+
}
452+
453+
return {
454+
value: [
455+
...flatten(
456+
filterMap(results, r =>
457+
r.status === 'fulfilled' && r.value != null && r.value?.error == null
458+
? r.value.value
459+
: undefined,
460+
),
461+
),
462+
],
463+
duration: Date.now() - start,
464+
};
438465
}
439466

440-
async getMyPullRequestsForRemotes(remote: GitRemote): Promise<SearchedPullRequest[] | undefined>;
441-
async getMyPullRequestsForRemotes(remotes: GitRemote[]): Promise<SearchedPullRequest[] | undefined>;
467+
async getMyPullRequestsForRemotes(remote: GitRemote): Promise<IntegrationResult<SearchedPullRequest[] | undefined>>;
468+
async getMyPullRequestsForRemotes(
469+
remotes: GitRemote[],
470+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>>;
442471
@debug<IntegrationService['getMyPullRequestsForRemotes']>({
443472
args: { 0: (r: GitRemote | GitRemote[]) => (Array.isArray(r) ? r.map(rp => rp.name) : r.name) },
444473
})
445474
async getMyPullRequestsForRemotes(
446475
remoteOrRemotes: GitRemote | GitRemote[],
447-
): Promise<SearchedPullRequest[] | undefined> {
476+
): Promise<IntegrationResult<SearchedPullRequest[] | undefined>> {
448477
if (!Array.isArray(remoteOrRemotes)) {
449478
remoteOrRemotes = [remoteOrRemotes];
450479
}

src/plus/integrations/providers/github/github.ts

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2728,6 +2728,12 @@ export class GitHubApi implements Disposable {
27282728
): Promise<SearchedPullRequest[]> {
27292729
const scope = getLogScope();
27302730

2731+
if (configuration.get('launchpad.experimental.queryUseInvolvesFilter') ?? false) {
2732+
return this.searchMyInvolvedPullRequests(provider, token, options, cancellation);
2733+
}
2734+
2735+
const limit = Math.min(100, configuration.get('launchpad.experimental.queryLimit') ?? 100);
2736+
27312737
interface SearchResult {
27322738
authored: {
27332739
nodes: GitHubPullRequest[];
@@ -2751,28 +2757,28 @@ export class GitHubApi implements Disposable {
27512757
$mentioned: String!
27522758
$avatarSize: Int
27532759
) {
2754-
authored: search(first: 100, query: $authored, type: ISSUE) {
2760+
authored: search(first: ${limit}, query: $authored, type: ISSUE) {
27552761
nodes {
27562762
...on PullRequest {
27572763
${gqlPullRequestFragment}
27582764
}
27592765
}
27602766
}
2761-
assigned: search(first: 100, query: $assigned, type: ISSUE) {
2767+
assigned: search(first: ${limit}, query: $assigned, type: ISSUE) {
27622768
nodes {
27632769
...on PullRequest {
27642770
${gqlPullRequestFragment}
27652771
}
27662772
}
27672773
}
2768-
reviewRequested: search(first: 100, query: $reviewRequested, type: ISSUE) {
2774+
reviewRequested: search(first: ${limit}, query: $reviewRequested, type: ISSUE) {
27692775
nodes {
27702776
...on PullRequest {
27712777
${gqlPullRequestFragment}
27722778
}
27732779
}
27742780
}
2775-
mentioned: search(first: 100, query: $mentioned, type: ISSUE) {
2781+
mentioned: search(first: ${limit}, query: $mentioned, type: ISSUE) {
27762782
nodes {
27772783
...on PullRequest {
27782784
${gqlPullRequestFragment}
@@ -2832,6 +2838,100 @@ export class GitHubApi implements Disposable {
28322838
}
28332839
}
28342840

2841+
@debug<GitHubApi['searchMyInvolvedPullRequests']>({ args: { 0: p => p.name, 1: '<token>' } })
2842+
private async searchMyInvolvedPullRequests(
2843+
provider: Provider,
2844+
token: string,
2845+
options?: { search?: string; user?: string; repos?: string[]; baseUrl?: string; avatarSize?: number },
2846+
cancellation?: CancellationToken,
2847+
): Promise<SearchedPullRequest[]> {
2848+
const scope = getLogScope();
2849+
2850+
const limit = Math.min(100, configuration.get('launchpad.experimental.queryLimit') ?? 100);
2851+
2852+
try {
2853+
interface SearchResult {
2854+
search: {
2855+
issueCount: number;
2856+
nodes: GitHubPullRequest[];
2857+
};
2858+
viewer: {
2859+
login: string;
2860+
};
2861+
}
2862+
2863+
const query = `query searchMyPullRequests(
2864+
$search: String!
2865+
$avatarSize: Int
2866+
) {
2867+
search(first: ${limit}, query: $search, type: ISSUE) {
2868+
issueCount
2869+
nodes {
2870+
...on PullRequest {
2871+
${gqlPullRequestFragment}
2872+
}
2873+
}
2874+
}
2875+
viewer {
2876+
login
2877+
}
2878+
}`;
2879+
2880+
let search = options?.search?.trim() ?? '';
2881+
2882+
if (options?.user) {
2883+
search += ` user:${options.user}`;
2884+
}
2885+
2886+
if (options?.repos != null && options.repos.length > 0) {
2887+
const repo = ' repo:';
2888+
search += `${repo}${options.repos.join(repo)}`;
2889+
}
2890+
2891+
const rsp = await this.graphql<SearchResult>(
2892+
provider,
2893+
token,
2894+
query,
2895+
{
2896+
search: `${search} is:pr is:open archived:false involves:@me`.trim(),
2897+
baseUrl: options?.baseUrl,
2898+
avatarSize: options?.avatarSize,
2899+
},
2900+
scope,
2901+
cancellation,
2902+
);
2903+
if (rsp == null) return [];
2904+
2905+
const viewer = rsp.viewer.login;
2906+
2907+
function toQueryResult(pr: GitHubPullRequest): SearchedPullRequest {
2908+
const reasons = [];
2909+
if (pr.author.login === viewer) {
2910+
reasons.push('authored');
2911+
}
2912+
if (pr.assignees.nodes.some(a => a.login === viewer)) {
2913+
reasons.push('assigned');
2914+
}
2915+
if (pr.reviewRequests.nodes.some(r => r.requestedReviewer?.login === viewer)) {
2916+
reasons.push('review-requested');
2917+
}
2918+
if (reasons.length === 0) {
2919+
reasons.push('mentioned');
2920+
}
2921+
2922+
return {
2923+
pullRequest: fromGitHubPullRequest(pr, provider),
2924+
reasons: reasons,
2925+
};
2926+
}
2927+
2928+
const results: SearchedPullRequest[] = rsp.search.nodes.map(pr => toQueryResult(pr));
2929+
return results;
2930+
} catch (ex) {
2931+
throw this.handleException(ex, provider, scope);
2932+
}
2933+
}
2934+
28352935
@debug<GitHubApi['searchMyIssues']>({ args: { 0: p => p.name, 1: '<token>' } })
28362936
async searchMyIssues(
28372937
provider: Provider,

src/plus/webviews/focus/focusWebview.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,9 +661,15 @@ export class FocusWebviewProvider implements WebviewProvider<State> {
661661
} catch (ex) {
662662
Logger.error(ex, scope, `Failed to get prs for '${r.remote.url}'`);
663663
}
664-
if (prs == null) continue;
665664

666-
for (const pr of prs) {
665+
if (prs?.error != null) {
666+
Logger.error(prs.error, scope, `Failed to get prs for '${r.remote.url}'`);
667+
continue;
668+
}
669+
670+
if (prs?.value == null) continue;
671+
672+
for (const pr of prs.value) {
667673
if (pr.reasons.length === 0) continue;
668674

669675
const entry: SearchedPullRequestWithRemote = {

0 commit comments

Comments
 (0)