Skip to content

Commit 157210f

Browse files
authored
Merge pull request #2439 from github/nora/search-prompt
Feed code search results into variant analysis repo lists
2 parents 0b6bcfd + 0739c46 commit 157210f

File tree

11 files changed

+402
-11
lines changed

11 files changed

+402
-11
lines changed

extensions/ql-vscode/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,10 @@
516516
"title": "Add new list",
517517
"icon": "$(new-folder)"
518518
},
519+
{
520+
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
521+
"title": "Add repositories with GitHub Code Search"
522+
},
519523
{
520524
"command": "codeQLVariantAnalysisRepositories.setSelectedItem",
521525
"title": "Select"
@@ -961,6 +965,11 @@
961965
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canBeOpenedOnGitHub/",
962966
"group": "2_qlContextMenu@1"
963967
},
968+
{
969+
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
970+
"when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/",
971+
"group": "2_qlContextMenu@1"
972+
},
964973
{
965974
"command": "codeQLDatabases.setCurrentDatabase",
966975
"group": "inline",
@@ -1297,6 +1306,10 @@
12971306
"command": "codeQLVariantAnalysisRepositories.removeItemContextMenu",
12981307
"when": "false"
12991308
},
1309+
{
1310+
"command": "codeQLVariantAnalysisRepositories.importFromCodeSearch",
1311+
"when": "false"
1312+
},
13001313
{
13011314
"command": "codeQLDatabases.setCurrentDatabase",
13021315
"when": "false"

extensions/ql-vscode/src/common/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export type DatabasePanelCommands = {
275275
"codeQLVariantAnalysisRepositories.openOnGitHubContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
276276
"codeQLVariantAnalysisRepositories.renameItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
277277
"codeQLVariantAnalysisRepositories.removeItemContextMenu": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
278+
"codeQLVariantAnalysisRepositories.importFromCodeSearch": TreeViewContextSingleSelectionCommandFunction<DbTreeViewItem>;
278279
};
279280

280281
export type AstCfgCommands = {

extensions/ql-vscode/src/databases/config/db-config-store.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,46 @@ export class DbConfigStore extends DisposableObject {
147147
await this.writeConfig(config);
148148
}
149149

150+
/**
151+
* Adds a list of remote repositories to an existing repository list and removes duplicates.
152+
* @returns a list of repositories that were not added because the list reached 1000 entries.
153+
*/
154+
public async addRemoteReposToList(
155+
repoNwoList: string[],
156+
parentList: string,
157+
): Promise<string[]> {
158+
if (!this.config) {
159+
throw Error("Cannot add variant analysis repos if config is not loaded");
160+
}
161+
162+
const config = cloneDbConfig(this.config);
163+
const parent = config.databases.variantAnalysis.repositoryLists.find(
164+
(list) => list.name === parentList,
165+
);
166+
if (!parent) {
167+
throw Error(`Cannot find parent list '${parentList}'`);
168+
}
169+
170+
// Remove duplicates from the list of repositories.
171+
const newRepositoriesList = [
172+
...new Set([...parent.repositories, ...repoNwoList]),
173+
];
174+
175+
parent.repositories = newRepositoriesList.slice(0, 1000);
176+
const truncatedRepositories = newRepositoriesList.slice(1000);
177+
178+
await this.writeConfig(config);
179+
return truncatedRepositories;
180+
}
181+
182+
/**
183+
* Adds one remote repository
184+
* @returns either nothing, or, if a parentList is given AND the number of repos on that list reaches 1000 returns the repo that was not added.
185+
*/
150186
public async addRemoteRepo(
151187
repoNwo: string,
152188
parentList?: string,
153-
): Promise<void> {
189+
): Promise<string[]> {
154190
if (!this.config) {
155191
throw Error("Cannot add variant analysis repo if config is not loaded");
156192
}
@@ -165,6 +201,7 @@ export class DbConfigStore extends DisposableObject {
165201
);
166202
}
167203

204+
const truncatedRepositories = [];
168205
const config = cloneDbConfig(this.config);
169206
if (parentList) {
170207
const parent = config.databases.variantAnalysis.repositoryLists.find(
@@ -173,12 +210,15 @@ export class DbConfigStore extends DisposableObject {
173210
if (!parent) {
174211
throw Error(`Cannot find parent list '${parentList}'`);
175212
} else {
176-
parent.repositories.push(repoNwo);
213+
const newRepositories = [...parent.repositories, repoNwo];
214+
parent.repositories = newRepositories.slice(0, 1000);
215+
truncatedRepositories.push(...newRepositories.slice(1000));
177216
}
178217
} else {
179218
config.databases.variantAnalysis.repositories.push(repoNwo);
180219
}
181220
await this.writeConfig(config);
221+
return truncatedRepositories;
182222
}
183223

184224
public async addRemoteOwner(owner: string): Promise<void> {

extensions/ql-vscode/src/databases/db-manager.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,15 @@ export class DbManager extends DisposableObject {
101101
public async addNewRemoteRepo(
102102
nwo: string,
103103
parentList?: string,
104-
): Promise<void> {
105-
await this.dbConfigStore.addRemoteRepo(nwo, parentList);
104+
): Promise<string[]> {
105+
return await this.dbConfigStore.addRemoteRepo(nwo, parentList);
106+
}
107+
108+
public async addNewRemoteReposToList(
109+
nwoList: string[],
110+
parentList: string,
111+
): Promise<string[]> {
112+
return await this.dbConfigStore.addRemoteReposToList(nwoList, parentList);
106113
}
107114

108115
public async addNewRemoteOwner(owner: string): Promise<void> {

extensions/ql-vscode/src/databases/ui/db-panel.ts

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ProgressLocation,
23
QuickPickItem,
34
TreeView,
45
TreeViewExpansionEvent,
@@ -13,7 +14,10 @@ import {
1314
getOwnerFromGitHubUrl,
1415
isValidGitHubOwner,
1516
} from "../../common/github-url-identifier-helper";
16-
import { showAndLogErrorMessage } from "../../helpers";
17+
import {
18+
showAndLogErrorMessage,
19+
showAndLogInformationMessage,
20+
} from "../../helpers";
1721
import { DisposableObject } from "../../pure/disposable-object";
1822
import {
1923
DbItem,
@@ -32,6 +36,8 @@ import { getControllerRepo } from "../../variant-analysis/run-remote-query";
3236
import { getErrorMessage } from "../../pure/helpers-pure";
3337
import { DatabasePanelCommands } from "../../common/commands";
3438
import { App } from "../../common/app";
39+
import { getCodeSearchRepositories } from "../../variant-analysis/gh-api/gh-api-client";
40+
import { QueryLanguage } from "../../common/query-language";
3541

3642
export interface RemoteDatabaseQuickPickItem extends QuickPickItem {
3743
remoteDatabaseKind: string;
@@ -41,6 +47,10 @@ export interface AddListQuickPickItem extends QuickPickItem {
4147
databaseKind: DbListKind;
4248
}
4349

50+
export interface CodeSearchQuickPickItem extends QuickPickItem {
51+
language: string;
52+
}
53+
4454
export class DbPanel extends DisposableObject {
4555
private readonly dataProvider: DbTreeDataProvider;
4656
private readonly treeView: TreeView<DbTreeViewItem>;
@@ -93,6 +103,8 @@ export class DbPanel extends DisposableObject {
93103
this.renameItem.bind(this),
94104
"codeQLVariantAnalysisRepositories.removeItemContextMenu":
95105
this.removeItem.bind(this),
106+
"codeQLVariantAnalysisRepositories.importFromCodeSearch":
107+
this.importFromCodeSearch.bind(this),
96108
};
97109
}
98110

@@ -171,7 +183,14 @@ export class DbPanel extends DisposableObject {
171183
return;
172184
}
173185

174-
await this.dbManager.addNewRemoteRepo(nwo, parentList);
186+
const truncatedRepositories = await this.dbManager.addNewRemoteRepo(
187+
nwo,
188+
parentList,
189+
);
190+
191+
if (parentList) {
192+
this.reportAnyTruncatedRepos(truncatedRepositories, parentList);
193+
}
175194
}
176195

177196
private async addNewRemoteOwner(): Promise<void> {
@@ -323,6 +342,89 @@ export class DbPanel extends DisposableObject {
323342
await this.dbManager.removeDbItem(treeViewItem.dbItem);
324343
}
325344

345+
private async importFromCodeSearch(
346+
treeViewItem: DbTreeViewItem,
347+
): Promise<void> {
348+
if (treeViewItem.dbItem?.kind !== DbItemKind.RemoteUserDefinedList) {
349+
throw new Error("Please select a valid list to add code search results.");
350+
}
351+
352+
const listName = treeViewItem.dbItem.listName;
353+
354+
const languageQuickPickItems: CodeSearchQuickPickItem[] = Object.values(
355+
QueryLanguage,
356+
).map((language) => ({
357+
label: language.toString(),
358+
alwaysShow: true,
359+
language: language.toString(),
360+
}));
361+
362+
const codeSearchLanguage =
363+
await window.showQuickPick<CodeSearchQuickPickItem>(
364+
languageQuickPickItems,
365+
{
366+
title: "Select a language for your search",
367+
placeHolder: "Select an option",
368+
ignoreFocusOut: true,
369+
},
370+
);
371+
if (!codeSearchLanguage) {
372+
return;
373+
}
374+
375+
const codeSearchQuery = await window.showInputBox({
376+
title: "GitHub Code Search",
377+
prompt:
378+
"Use [GitHub's Code Search syntax](https://docs.github.com/en/search-github/github-code-search/understanding-github-code-search-syntax), including code qualifiers, regular expressions, and boolean operations, to search for repositories.",
379+
placeHolder: "org:github",
380+
});
381+
if (codeSearchQuery === undefined || codeSearchQuery === "") {
382+
return;
383+
}
384+
385+
void window.withProgress(
386+
{
387+
location: ProgressLocation.Notification,
388+
title: "Searching for repositories... This might take a while",
389+
cancellable: true,
390+
},
391+
async (progress, token) => {
392+
progress.report({ increment: 10 });
393+
394+
const repositories = await getCodeSearchRepositories(
395+
this.app.credentials,
396+
`${codeSearchQuery} language:${codeSearchLanguage.language}`,
397+
progress,
398+
token,
399+
);
400+
401+
token.onCancellationRequested(() => {
402+
void showAndLogInformationMessage("Code search cancelled");
403+
return;
404+
});
405+
406+
progress.report({ increment: 10, message: "Processing results..." });
407+
408+
const truncatedRepositories =
409+
await this.dbManager.addNewRemoteReposToList(repositories, listName);
410+
this.reportAnyTruncatedRepos(truncatedRepositories, listName);
411+
},
412+
);
413+
}
414+
415+
private reportAnyTruncatedRepos(
416+
truncatedRepositories: string[],
417+
listName: string,
418+
) {
419+
if (truncatedRepositories.length > 0) {
420+
void showAndLogErrorMessage(
421+
`Some repositories were not added to '${listName}' because a list can only have 1000 entries. Excluded repositories: ${truncatedRepositories.join(
422+
", ",
423+
)}`,
424+
);
425+
}
426+
}
427+
326428
private async onDidCollapseElement(
327429
event: TreeViewExpansionEvent<DbTreeViewItem>,
328430
): Promise<void> {

extensions/ql-vscode/src/databases/ui/db-tree-view-item-action.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export type DbTreeViewItemAction =
44
| "canBeSelected"
55
| "canBeRemoved"
66
| "canBeRenamed"
7-
| "canBeOpenedOnGitHub";
7+
| "canBeOpenedOnGitHub"
8+
| "canImportCodeSearch";
89

910
export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
1011
const actions: DbTreeViewItemAction[] = [];
@@ -21,7 +22,9 @@ export function getDbItemActions(dbItem: DbItem): DbTreeViewItemAction[] {
2122
if (canBeOpenedOnGitHub(dbItem)) {
2223
actions.push("canBeOpenedOnGitHub");
2324
}
24-
25+
if (canImportCodeSearch(dbItem)) {
26+
actions.push("canImportCodeSearch");
27+
}
2528
return actions;
2629
}
2730

@@ -60,6 +63,10 @@ function canBeOpenedOnGitHub(dbItem: DbItem): boolean {
6063
return dbItemKindsThatCanBeOpenedOnGitHub.includes(dbItem.kind);
6164
}
6265

66+
function canImportCodeSearch(dbItem: DbItem): boolean {
67+
return DbItemKind.RemoteUserDefinedList === dbItem.kind;
68+
}
69+
6370
export function getGitHubUrl(dbItem: DbItem): string | undefined {
6471
switch (dbItem.kind) {
6572
case DbItemKind.RemoteOwner:

extensions/ql-vscode/src/variant-analysis/gh-api/gh-api-client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ import {
77
VariantAnalysisSubmissionRequest,
88
} from "./variant-analysis";
99
import { Repository } from "./repository";
10+
import { Progress } from "vscode";
11+
import { CancellationToken } from "vscode-jsonrpc";
12+
13+
export async function getCodeSearchRepositories(
14+
credentials: Credentials,
15+
query: string,
16+
progress: Progress<{
17+
message?: string | undefined;
18+
increment?: number | undefined;
19+
}>,
20+
token: CancellationToken,
21+
): Promise<string[]> {
22+
let nwos: string[] = [];
23+
const octokit = await credentials.getOctokit();
24+
for await (const response of octokit.paginate.iterator(
25+
octokit.rest.search.repos,
26+
{
27+
q: query,
28+
per_page: 100,
29+
},
30+
)) {
31+
nwos.push(...response.data.map((item) => item.full_name));
32+
// calculate progress bar: 80% of the progress bar is used for the code search
33+
const totalNumberOfRequests = Math.ceil(response.data.total_count / 100);
34+
// Since we have a maximum 10 of requests, we use a fixed increment whenever the totalNumberOfRequests is greater than 10
35+
const increment =
36+
totalNumberOfRequests < 10 ? 80 / totalNumberOfRequests : 8;
37+
progress.report({ increment });
38+
39+
if (token.isCancellationRequested) {
40+
nwos = [];
41+
break;
42+
}
43+
}
44+
45+
return [...new Set(nwos)];
46+
}
1047

1148
export async function submitVariantAnalysis(
1249
credentials: Credentials,

0 commit comments

Comments
 (0)