Skip to content

Commit 3070b6a

Browse files
authored
Support global PR queries (#6584)
* Support global PR queries Fixes #6448 * Fix test
1 parent 5a923cf commit 3070b6a

10 files changed

Lines changed: 157 additions & 89 deletions

File tree

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,15 @@
184184
"default": [
185185
{
186186
"label": "%githubPullRequests.queries.waitingForMyReview%",
187-
"query": "is:open review-requested:${user}"
187+
"query": "repo:${owner}/${repository} is:open review-requested:${user}"
188188
},
189189
{
190190
"label": "%githubPullRequests.queries.assignedToMe%",
191-
"query": "is:open assignee:${user}"
191+
"query": "repo:${owner}/${repository} is:open assignee:${user}"
192192
},
193193
{
194194
"label": "%githubPullRequests.queries.createdByMe%",
195-
"query": "is:open author:${user}"
195+
"query": "repo:${owner}/${repository} is:open author:${user}"
196196
}
197197
]
198198
},

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Repository } from './api/api';
1010
import { GitErrorCodes } from './api/api1';
1111
import { CommentReply, findActiveHandler, resolveCommentHandler } from './commentHandlerResolver';
1212
import { IComment } from './common/comment';
13+
import { commands } from './common/executeCommands';
1314
import Logger from './common/logger';
1415
import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE } from './common/settingKeys';
1516
import { ITelemetry } from './common/telemetry';
@@ -44,7 +45,6 @@ import {
4445
} from './view/treeNodes/fileChangeNode';
4546
import { PRNode } from './view/treeNodes/pullRequestNode';
4647
import { RepositoryChangesNode } from './view/treeNodes/repositoryChangesNode';
47-
import { commands } from './common/executeCommands';
4848

4949
const _onDidUpdatePR = new vscode.EventEmitter<PullRequest | void>();
5050
export const onDidUpdatePR: vscode.Event<PullRequest | void> = _onDidUpdatePR.event;

src/extension.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { GitLensIntegration } from './integrations/gitlens/gitlensImpl';
2929
import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar';
3030
import { ChatParticipant, ChatParticipantState } from './lm/participants';
3131
import { registerTools } from './lm/tools/tools';
32+
import { migrate } from './migrations';
3233
import { NotificationsFeatureRegister } from './notifications/notificationsFeatureRegistar';
3334
import { CommentDecorationProvider } from './view/commentDecorationProvider';
3435
import { CompareChanges } from './view/compareChangesTreeDataProvider';
@@ -47,9 +48,6 @@ const ingestionKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-f
4748

4849
let telemetry: ExperimentationTelemetry;
4950

50-
const PROMPTS_SCOPE = 'prompts';
51-
const PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY = 'createPROnPublish';
52-
5351
async function init(
5452
context: vscode.ExtensionContext,
5553
git: GitApiImpl,
@@ -385,11 +383,7 @@ async function deferredActivateRegisterBuiltInGitProvider(context: vscode.Extens
385383
async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitApiImpl, showPRController: ShowPullRequest) {
386384
Logger.debug('Initializing state.', 'Activation');
387385
PersistentState.init(context);
388-
// Migrate from state to setting
389-
if (PersistentState.fetch(PROMPTS_SCOPE, PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY) === false) {
390-
await vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).update(BRANCH_PUBLISH, 'never', vscode.ConfigurationTarget.Global);
391-
PersistentState.store(PROMPTS_SCOPE, PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY, true);
392-
}
386+
await migrate(context);
393387
TemporaryState.init(context);
394388
Logger.debug('Creating credential store.', 'Activation');
395389
const credentialStore = new CredentialStore(telemetry, context);

src/github/folderRepositoryManager.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import { ConflictModel } from './conflictGuide';
4141
import { ConflictResolutionCoordinator } from './conflictResolutionCoordinator';
4242
import { Conflict, ConflictResolutionModel } from './conflictResolutionModel';
4343
import { CredentialStore } from './credentials';
44-
import { GitHubRepository, GraphQLError, GraphQLErrorType, ItemsData, PullRequestData, TeamReviewerRefreshKind, ViewerPermission } from './githubRepository';
45-
import { MergeMethod as GraphQLMergeMethod, MergePullRequestInput, MergePullRequestResponse, PullRequestState, UserResponse } from './graphql';
44+
import { GitHubRepository, GraphQLError, GraphQLErrorType, IMetadata, ItemsData, PULL_REQUEST_PAGE_SIZE, PullRequestData, TeamReviewerRefreshKind, ViewerPermission } from './githubRepository';
45+
import { MergeMethod as GraphQLMergeMethod, MergePullRequestInput, MergePullRequestResponse, PullRequestResponse, PullRequestState, UserResponse } from './graphql';
4646
import { IAccount, ILabel, IMilestone, IProject, IPullRequestsPagingOptions, Issue, ITeam, MergeMethod, PRType, PullRequestMergeability, RepoAccessAndMergeMethods, User } from './interface';
4747
import { IssueModel } from './issueModel';
4848
import { PullRequestGitHelper, PullRequestMetadata } from './pullRequestGitHelper';
@@ -51,7 +51,9 @@ import {
5151
convertRESTIssueToRawPullRequest,
5252
convertRESTPullRequestToRawPullRequest,
5353
getOverrideBranch,
54+
getPRFetchQuery,
5455
loginComparator,
56+
parseGraphQLPullRequest,
5557
parseGraphQLTimelineEvents,
5658
parseGraphQLUser,
5759
teamComparator,
@@ -1085,7 +1087,7 @@ export class FolderRepositoryManager extends Disposable {
10851087
if (type === PRType.All) {
10861088
return githubRepository.getAllPullRequests(pageNumber);
10871089
} else {
1088-
return githubRepository.getPullRequestsForCategory(resolvedQuery || '', pageNumber);
1090+
return this.getPullRequestsForCategory(githubRepository, resolvedQuery || '', pageNumber);
10891091
}
10901092
}
10911093
case PagedDataType.IssueSearch: {
@@ -1146,6 +1148,85 @@ export class FolderRepositoryManager extends Disposable {
11461148
};
11471149
}
11481150

1151+
async getPullRequestsForCategory(githubRepository: GitHubRepository, categoryQuery: string, page?: number): Promise<PullRequestData | undefined> {
1152+
let repo: IMetadata | undefined;
1153+
try {
1154+
Logger.debug(`Fetch pull request category ${categoryQuery} - enter`, this.id);
1155+
const { octokit, query, schema } = await githubRepository.ensure();
1156+
1157+
const user = await githubRepository.getAuthenticatedUser();
1158+
// Search api will not try to resolve repo that redirects, so get full name first
1159+
repo = await githubRepository.getMetadata();
1160+
const { data, headers } = await octokit.call(octokit.api.search.issuesAndPullRequests, {
1161+
q: getPRFetchQuery(user, categoryQuery),
1162+
per_page: PULL_REQUEST_PAGE_SIZE,
1163+
page: page || 1,
1164+
});
1165+
1166+
const promises: Promise<{ data: PullRequestResponse, repo: GitHubRepository } | undefined>[] = data.items.map(async (item) => {
1167+
const protocol = new Protocol(item.repository_url);
1168+
1169+
const prRepo = await this.createGitHubRepositoryFromOwnerName(protocol.owner, protocol.repositoryName);
1170+
const { data } = await query<PullRequestResponse>({
1171+
query: schema.PullRequest,
1172+
variables: {
1173+
owner: prRepo.remote.owner,
1174+
name: prRepo.remote.repositoryName,
1175+
number: item.number
1176+
}
1177+
});
1178+
return { data, repo: prRepo };
1179+
});
1180+
1181+
const hasMorePages = !!headers.link && headers.link.indexOf('rel="next"') > -1;
1182+
const pullRequestResponses = await Promise.all(promises);
1183+
1184+
const pullRequests = pullRequestResponses
1185+
.map(response => {
1186+
if (!response?.data.repository) {
1187+
Logger.appendLine('Pull request doesn\'t appear to exist.', this.id);
1188+
return null;
1189+
}
1190+
1191+
// Pull requests fetched with a query can be from any repo.
1192+
// We need to use the correct GitHubRepository for this PR.
1193+
return response.repo.createOrUpdatePullRequestModel(
1194+
parseGraphQLPullRequest(response.data.repository.pullRequest, response.repo),
1195+
);
1196+
})
1197+
.filter(item => item !== null) as PullRequestModel[];
1198+
1199+
Logger.debug(`Fetch pull request category ${categoryQuery} - done`, this.id);
1200+
1201+
return {
1202+
items: pullRequests,
1203+
hasMorePages,
1204+
totalCount: data.total_count
1205+
};
1206+
} catch (e) {
1207+
Logger.error(`Fetching pull request with query failed: ${e}`, this.id);
1208+
if (e.code === 404) {
1209+
// not found
1210+
vscode.window.showWarningMessage(
1211+
`Fetching pull requests for remote ${githubRepository.remote.remoteName} with query failed, please check if the repo ${repo?.full_name} is valid.`,
1212+
);
1213+
} else {
1214+
throw e;
1215+
}
1216+
}
1217+
return undefined;
1218+
}
1219+
1220+
isPullRequestAssociatedWithOpenRepository(pullRequest: PullRequestModel): boolean {
1221+
const remote = pullRequest.githubRepository.remote;
1222+
const repository = this.repository.state.remotes.find(repo => repo.name === remote.remoteName);
1223+
if (repository) {
1224+
return true;
1225+
}
1226+
1227+
return false;
1228+
}
1229+
11491230
async getPullRequests(
11501231
type: PRType,
11511232
options: IPullRequestsPagingOptions = { fetchNextPage: false },

src/github/githubRepository.ts

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as vscode from 'vscode';
99
import { AuthenticationError, AuthProvider, GitHubServerType, isSamlError } from '../common/authentication';
1010
import { Disposable } from '../common/lifecycle';
1111
import Logger from '../common/logger';
12-
import { Protocol } from '../common/protocol';
1312
import { GitHubRemote, parseRemote } from '../common/remote';
1413
import { ITelemetry } from '../common/telemetry';
1514
import { PRCommentControllerRegistry } from '../view/pullRequestCommentControllerRegistry';
@@ -64,7 +63,6 @@ import {
6463
convertRESTPullRequestToRawPullRequest,
6564
getAvatarWithEnterpriseFallback,
6665
getOverrideBranch,
67-
getPRFetchQuery,
6866
isInCodespaces,
6967
parseGraphQLIssue,
7068
parseGraphQLPullRequest,
@@ -882,71 +880,6 @@ export class GitHubRepository extends Disposable {
882880
}
883881
}
884882

885-
async getPullRequestsForCategory(categoryQuery: string, page?: number): Promise<PullRequestData | undefined> {
886-
let repo: IMetadata | undefined;
887-
try {
888-
Logger.debug(`Fetch pull request category ${categoryQuery} - enter`, this.id);
889-
const { octokit, query, schema } = await this.ensure();
890-
891-
const user = await this.getAuthenticatedUser();
892-
// Search api will not try to resolve repo that redirects, so get full name first
893-
repo = await this.getMetadata();
894-
const { data, headers } = await octokit.call(octokit.api.search.issuesAndPullRequests, {
895-
q: getPRFetchQuery(repo.full_name, user, categoryQuery),
896-
per_page: PULL_REQUEST_PAGE_SIZE,
897-
page: page || 1,
898-
});
899-
900-
const promises: Promise<PullRequestResponse>[] = data.items.map(async (item) => {
901-
const prRepo = new Protocol(item.repository_url);
902-
const { data } = await query<PullRequestResponse>({
903-
query: schema.PullRequest,
904-
variables: {
905-
owner: prRepo.owner,
906-
name: prRepo.repositoryName,
907-
number: item.number
908-
}
909-
});
910-
return data;
911-
});
912-
913-
const hasMorePages = !!headers.link && headers.link.indexOf('rel="next"') > -1;
914-
const pullRequestResponses = await Promise.all(promises);
915-
916-
const pullRequests = pullRequestResponses
917-
.map(response => {
918-
if (!response.repository) {
919-
Logger.appendLine('Pull request doesn\'t appear to exist.', this.id);
920-
return null;
921-
}
922-
923-
return this.createOrUpdatePullRequestModel(
924-
parseGraphQLPullRequest(response.repository.pullRequest, this),
925-
);
926-
})
927-
.filter(item => item !== null) as PullRequestModel[];
928-
929-
Logger.debug(`Fetch pull request category ${categoryQuery} - done`, this.id);
930-
931-
return {
932-
items: pullRequests,
933-
hasMorePages,
934-
totalCount: data.total_count
935-
};
936-
} catch (e) {
937-
Logger.error(`Fetching pull request with query failed: ${e}`, this.id);
938-
if (e.code === 404) {
939-
// not found
940-
vscode.window.showWarningMessage(
941-
`Fetching pull requests for remote ${this.remote.remoteName} with query failed, please check if the repo ${repo?.full_name} is valid.`,
942-
);
943-
} else {
944-
throw e;
945-
}
946-
}
947-
return undefined;
948-
}
949-
950883
createOrUpdatePullRequestModel(pullRequest: PullRequest): PullRequestModel {
951884
let model = this._pullRequestModels.get(pullRequest.number);
952885
if (model) {

src/github/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,9 +1360,9 @@ export function insertNewCommitsSinceReview(
13601360
}
13611361
}
13621362

1363-
export function getPRFetchQuery(repo: string, user: string, query: string): string {
1363+
export function getPRFetchQuery(user: string, query: string): string {
13641364
const filter = query.replace(/\$\{user\}/g, user);
1365-
return `is:pull-request ${filter} type:pr repo:${repo}`;
1365+
return `is:pull-request ${filter} type:pr`;
13661366
}
13671367

13681368
export function isInCodespaces(): boolean {

src/migrations.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import * as PersistentState from './common/persistentState';
8+
import { BRANCH_PUBLISH, PR_SETTINGS_NAMESPACE, QUERIES } from './common/settingKeys';
9+
10+
const PROMPTS_SCOPE = 'prompts';
11+
const PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY = 'createPROnPublish';
12+
13+
export async function migrate(context: vscode.ExtensionContext) {
14+
await createOnPublish();
15+
await makeQueriesScopedToRepo(context);
16+
}
17+
18+
async function createOnPublish() {
19+
// Migrate from state to setting
20+
if (PersistentState.fetch(PROMPTS_SCOPE, PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY) === false) {
21+
await vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).update(BRANCH_PUBLISH, 'never', vscode.ConfigurationTarget.Global);
22+
PersistentState.store(PROMPTS_SCOPE, PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY, true);
23+
}
24+
}
25+
26+
const HAS_MIGRATED_QUERIES = 'hasMigratedQueries';
27+
async function makeQueriesScopedToRepo(context: vscode.ExtensionContext) {
28+
const hasMigratedUserQueries = context.globalState.get<boolean>(HAS_MIGRATED_QUERIES, false);
29+
const hasMigratedWorkspaceQueries = context.workspaceState.get<boolean>(HAS_MIGRATED_QUERIES, false);
30+
if (hasMigratedUserQueries && hasMigratedWorkspaceQueries) {
31+
return;
32+
}
33+
34+
const configuration = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE);
35+
const settingValue = configuration.inspect(QUERIES);
36+
37+
type Query = {
38+
label: string,
39+
query: string,
40+
};
41+
const addRepoScope = (queries: Query[]) => {
42+
return queries.map(query => {
43+
return {
44+
label: query.label,
45+
query: query.query.includes('repo:') ? query.query : `repo:\${owner}/\${repository} ${query.query}`,
46+
};
47+
});
48+
};
49+
50+
// User setting
51+
if (!hasMigratedUserQueries && settingValue?.globalValue) {
52+
await configuration.update(QUERIES, addRepoScope(settingValue.globalValue as Query[]), vscode.ConfigurationTarget.Global);
53+
context.globalState.update(HAS_MIGRATED_QUERIES, true);
54+
}
55+
56+
// Workspace setting
57+
if (!hasMigratedWorkspaceQueries && settingValue?.workspaceValue) {
58+
await configuration.update(QUERIES, addRepoScope(settingValue.workspaceValue as Query[]), vscode.ConfigurationTarget.Workspace);
59+
context.workspaceState.update(HAS_MIGRATED_QUERIES, true);
60+
}
61+
}

src/test/github/utils.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ describe('utils', () => {
1010

1111
describe('getPRFetchQuery', () => {
1212
it('replaces all instances of ${user}', () => {
13-
const repo = 'microsoft/vscode-pull-request-github';
1413
const user = 'rmacfarlane';
1514
const query = 'reviewed-by:${user} -author:${user}';
16-
const result = getPRFetchQuery(repo, user, query)
17-
assert.strictEqual(result, 'is:pull-request reviewed-by:rmacfarlane -author:rmacfarlane type:pr repo:microsoft/vscode-pull-request-github');
15+
const result = getPRFetchQuery(user, query)
16+
assert.strictEqual(result, 'is:pull-request reviewed-by:rmacfarlane -author:rmacfarlane type:pr');
1817
});
1918
});
2019

src/view/treeNodes/descriptionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ export class DescriptionNode extends TreeNode implements vscode.TreeItem {
4646
(currentBranchIsForThisPR ? ':active' : ':nonactive') +
4747
(this.pullRequestModel.hasChangesSinceLastReview ? ':hasChangesSinceReview' : '') +
4848
(this.pullRequestModel.showChangesSinceReview ? ':showingChangesSinceReview' : ':showingAllChanges') +
49-
(this.pullRequestModel.item.isRemoteHeadDeleted ? '' : ':hasHeadRef');
49+
((this.pullRequestModel.item.isRemoteHeadDeleted || !this.folderRepositoryManager.isPullRequestAssociatedWithOpenRepository(this.pullRequestModel)) ? '' : ':hasHeadRef');
5050
}
5151
}

src/view/treeNodes/pullRequestNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2
305305
(this._isLocal ? ':local' : '') +
306306
(currentBranchIsForThisPR ? ':active' : ':nonactive') +
307307
(hasNotification ? ':notification' : '') +
308-
(this.pullRequestModel.item.isRemoteHeadDeleted ? '' : ':hasHeadRef'),
308+
((this.pullRequestModel.item.isRemoteHeadDeleted || !this._folderReposManager.isPullRequestAssociatedWithOpenRepository(this.pullRequestModel)) ? '' : ':hasHeadRef'),
309309
iconPath: (await DataUri.avatarCirclesAsImageDataUris(this._folderReposManager.context, [this.pullRequestModel.author], 16, 16))[0]
310310
?? new vscode.ThemeIcon('github'),
311311
accessibilityInformation: {

0 commit comments

Comments
 (0)