Skip to content

Commit 22ed090

Browse files
authored
Add support for system defined repository lists (#1271)
1 parent 2ca4097 commit 22ed090

File tree

4 files changed

+162
-61
lines changed

4 files changed

+162
-61
lines changed
Lines changed: 102 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,116 @@
11
import { QuickPickItem, window } from 'vscode';
22
import { showAndLogErrorMessage } from '../helpers';
3-
import { getRemoteRepositoryLists } from '../config';
43
import { logger } from '../logging';
4+
import { getRemoteRepositoryLists } from '../config';
55
import { REPO_REGEX } from '../pure/helpers-pure';
66

7+
export interface RepositorySelection {
8+
repositories?: string[];
9+
repositoryLists?: string[]
10+
}
11+
712
interface RepoListQuickPickItem extends QuickPickItem {
8-
repoList: string[];
13+
repositories?: string[];
14+
repositoryList?: string;
15+
useCustomRepository?: boolean;
916
}
1017

1118
/**
12-
* Gets the repositories to run the query against.
19+
* Gets the repositories or repository lists to run the query against.
20+
* @returns The user selection.
1321
*/
14-
export async function getRepositories(): Promise<string[] | undefined> {
15-
const repoLists = getRemoteRepositoryLists();
16-
if (repoLists && Object.keys(repoLists).length) {
17-
const quickPickItems = Object.entries(repoLists).map<RepoListQuickPickItem>(([key, value]) => (
18-
{
19-
label: key, // the name of the repository list
20-
repoList: value, // the actual array of repositories
21-
}
22-
));
23-
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
24-
quickPickItems,
25-
{
26-
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.',
27-
ignoreFocusOut: true,
28-
});
29-
if (quickpick?.repoList.length) {
30-
void logger.log(`Selected repositories: ${quickpick.repoList.join(', ')}`);
31-
return quickpick.repoList;
32-
} else {
33-
void showAndLogErrorMessage('No repositories selected.');
34-
return;
22+
export async function getRepositorySelection(): Promise<RepositorySelection> {
23+
const quickPickItems = [
24+
createCustomRepoQuickPickItem(),
25+
...createSystemDefinedRepoListsQuickPickItems(),
26+
...createUserDefinedRepoListsQuickPickItems(),
27+
];
28+
29+
const options = {
30+
placeHolder: 'Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.',
31+
ignoreFocusOut: true,
32+
};
33+
34+
const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
35+
quickPickItems,
36+
options);
37+
38+
if (quickpick?.repositories?.length) {
39+
void logger.log(`Selected repositories: ${quickpick.repositories.join(', ')}`);
40+
return { repositories: quickpick.repositories };
41+
} else if (quickpick?.repositoryList) {
42+
void logger.log(`Selected repository list: ${quickpick.repositoryList}`);
43+
return { repositoryLists: [quickpick.repositoryList] };
44+
} else if (quickpick?.useCustomRepository) {
45+
const customRepo = await getCustomRepo();
46+
if (!customRepo || !REPO_REGEX.test(customRepo)) {
47+
void showAndLogErrorMessage('Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)');
48+
return {};
3549
}
50+
void logger.log(`Entered repository: ${customRepo}`);
51+
return { repositories: [customRepo] };
3652
} else {
37-
void logger.log('No repository lists defined. Displaying text input box.');
38-
const remoteRepo = await window.showInputBox({
39-
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
40-
placeHolder: '<owner>/<repo>',
41-
prompt: 'Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting',
42-
ignoreFocusOut: true,
43-
});
44-
if (!remoteRepo) {
45-
void showAndLogErrorMessage('No repositories entered.');
46-
return;
47-
} else if (!REPO_REGEX.test(remoteRepo)) { // Check if user entered invalid input
48-
void showAndLogErrorMessage('Invalid repository format. Must be in the format <owner>/<repo> (e.g. github/codeql)');
49-
return;
50-
}
51-
void logger.log(`Entered repository: ${remoteRepo}`);
52-
return [remoteRepo];
53+
void showAndLogErrorMessage('No repositories selected.');
54+
return {};
55+
}
56+
}
57+
58+
/**
59+
* Checks if the selection is valid or not.
60+
* @param repoSelection The selection to check.
61+
* @returns A boolean flag indicating if the selection is valid or not.
62+
*/
63+
export function isValidSelection(repoSelection: RepositorySelection): boolean {
64+
if (repoSelection.repositories === undefined && repoSelection.repositoryLists === undefined) {
65+
return false;
66+
}
67+
if (repoSelection.repositories !== undefined && repoSelection.repositories.length === 0) {
68+
return false;
5369
}
70+
if (repoSelection.repositoryLists?.length === 0) {
71+
return false;
72+
}
73+
74+
return true;
75+
}
76+
77+
function createSystemDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
78+
const topNs = [10, 100, 1000];
79+
80+
return topNs.map(n => ({
81+
label: '$(star) Top ' + n,
82+
repositoryList: `top_${n}`,
83+
alwaysShow: true
84+
} as RepoListQuickPickItem));
85+
}
86+
87+
function createUserDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
88+
const repoLists = getRemoteRepositoryLists();
89+
if (!repoLists) {
90+
return [];
91+
}
92+
93+
return Object.entries(repoLists).map<RepoListQuickPickItem>(([label, repositories]) => (
94+
{
95+
label, // the name of the repository list
96+
repositories // the actual array of repositories
97+
}
98+
));
99+
}
100+
101+
function createCustomRepoQuickPickItem(): RepoListQuickPickItem {
102+
return {
103+
label: '$(edit) Enter a GitHub repository',
104+
useCustomRepository: true,
105+
alwaysShow: true,
106+
};
107+
}
108+
109+
async function getCustomRepo(): Promise<string | undefined> {
110+
return await window.showInputBox({
111+
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
112+
placeHolder: '<owner>/<repo>',
113+
prompt: 'Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting',
114+
ignoreFocusOut: true,
115+
});
54116
}

extensions/ql-vscode/src/remote-queries/run-remote-query.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { RemoteQuery } from './remote-query';
2222
import { RemoteQuerySubmissionResult } from './remote-query-submission-result';
2323
import { QueryMetadata } from '../pure/interface-types';
2424
import { getErrorMessage, REPO_REGEX } from '../pure/helpers-pure';
25-
import { getRepositories } from './repository-selection';
25+
import { getRepositorySelection, isValidSelection, RepositorySelection } from './repository-selection';
2626

2727
export interface QlPack {
2828
name: string;
@@ -189,8 +189,8 @@ export async function runRemoteQuery(
189189
message: 'Determining query target language'
190190
});
191191

192-
const repositories = await getRepositories();
193-
if (!repositories || repositories.length === 0) {
192+
const repoSelection = await getRepositorySelection();
193+
if (!isValidSelection(repoSelection)) {
194194
throw new UserCancellationException('No repositories to query.');
195195
}
196196

@@ -249,7 +249,7 @@ export async function runRemoteQuery(
249249
});
250250

251251
const actionBranch = getActionBranch();
252-
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repositories, owner, repo, base64Pack, dryRun);
252+
const workflowRunId = await runRemoteQueriesApiRequest(credentials, actionBranch, language, repoSelection, owner, repo, base64Pack, dryRun);
253253
const queryStartTime = Date.now();
254254
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
255255

@@ -287,15 +287,30 @@ async function runRemoteQueriesApiRequest(
287287
credentials: Credentials,
288288
ref: string,
289289
language: string,
290-
repositories: string[],
290+
repoSelection: RepositorySelection,
291291
owner: string,
292292
repo: string,
293293
queryPackBase64: string,
294294
dryRun = false
295295
): Promise<void | number> {
296+
const data = {
297+
ref,
298+
language,
299+
repositories: repoSelection.repositories ?? undefined,
300+
repository_lists: repoSelection.repositoryLists ?? undefined,
301+
query_pack: queryPackBase64,
302+
};
303+
296304
if (dryRun) {
297305
void showAndLogInformationMessage('[DRY RUN] Would have sent request. See extension log for the payload.');
298-
void logger.log(JSON.stringify({ ref, language, repositories, owner, repo, queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes' }));
306+
void logger.log(JSON.stringify({
307+
owner,
308+
repo,
309+
data: {
310+
...data,
311+
queryPackBase64: queryPackBase64.substring(0, 100) + '... ' + queryPackBase64.length + ' bytes'
312+
}
313+
}));
299314
return;
300315
}
301316

@@ -306,12 +321,7 @@ async function runRemoteQueriesApiRequest(
306321
{
307322
owner,
308323
repo,
309-
data: {
310-
ref,
311-
language,
312-
repositories,
313-
query_pack: queryPackBase64,
314-
}
324+
data
315325
}
316326
);
317327
const workflowRunId = response.data.workflow_run_id;

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('Remote queries', function() {
5252
progress = sandbox.spy();
5353
// Should not have asked for a language
5454
showQuickPickSpy = sandbox.stub(window, 'showQuickPick')
55-
.onFirstCall().resolves({ repoList: ['github/vscode-codeql'] } as unknown as QuickPickItem)
55+
.onFirstCall().resolves({ repositories: ['github/vscode-codeql'] } as unknown as QuickPickItem)
5656
.onSecondCall().resolves('javascript' as unknown as QuickPickItem);
5757

5858
// always run in the vscode-codeql repo

extensions/ql-vscode/src/vscode-tests/no-workspace/remote-queries/repository-selection.test.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const proxyquire = pq.noPreserveCache();
88

99
describe('repository-selection', function() {
1010

11-
describe('getRepositories', () => {
11+
describe('getRepositorySelection', () => {
1212
let sandbox: sinon.SinonSandbox;
1313
let quickPickSpy: sinon.SinonStub;
1414
let showInputBoxSpy: sinon.SinonStub;
@@ -35,10 +35,10 @@ describe('repository-selection', function() {
3535
sandbox.restore();
3636
});
3737

38-
it('should run on a repo list that you chose from your pre-defined config', async () => {
38+
it('should allow selection from repo lists from your pre-defined config', async () => {
3939
// fake return values
4040
quickPickSpy.resolves(
41-
{ repoList: ['foo/bar', 'foo/baz'] }
41+
{ repositories: ['foo/bar', 'foo/baz'] }
4242
);
4343
getRemoteRepositoryListsSpy.returns(
4444
{
@@ -48,14 +48,37 @@ describe('repository-selection', function() {
4848
);
4949

5050
// make the function call
51-
const repoList = await mod.getRepositories();
51+
const repoSelection = await mod.getRepositorySelection();
5252

5353
// Check that the return value is correct
54-
expect(repoList).to.deep.eq(
54+
expect(repoSelection.repositoryLists).to.be.undefined;
55+
expect(repoSelection.repositories).to.deep.eq(
5556
['foo/bar', 'foo/baz']
5657
);
5758
});
5859

60+
it('should allow selection from repo lists defined at the system level', async () => {
61+
// fake return values
62+
quickPickSpy.resolves(
63+
{ repositoryList: 'top_100' }
64+
);
65+
getRemoteRepositoryListsSpy.returns(
66+
{
67+
'list1': ['foo/bar', 'foo/baz'],
68+
'list2': [],
69+
}
70+
);
71+
72+
// make the function call
73+
const repoSelection = await mod.getRepositorySelection();
74+
75+
// Check that the return value is correct
76+
expect(repoSelection.repositories).to.be.undefined;
77+
expect(repoSelection.repositoryLists).to.deep.eq(
78+
['top_100']
79+
);
80+
});
81+
5982
// Test the regex in various "good" cases
6083
const goodRepos = [
6184
'owner/repo',
@@ -65,14 +88,17 @@ describe('repository-selection', function() {
6588
goodRepos.forEach(repo => {
6689
it(`should run on a valid repo that you enter in the text box: ${repo}`, async () => {
6790
// fake return values
91+
quickPickSpy.resolves(
92+
{ useCustomRepository: true }
93+
);
6894
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
6995
showInputBoxSpy.resolves(repo);
7096

7197
// make the function call
72-
const repoList = await mod.getRepositories();
98+
const repoSelection = await mod.getRepositorySelection();
7399

74100
// Check that the return value is correct
75-
expect(repoList).to.deep.equal(
101+
expect(repoSelection.repositories).to.deep.equal(
76102
[repo]
77103
);
78104
});
@@ -88,11 +114,14 @@ describe('repository-selection', function() {
88114
badRepos.forEach(repo => {
89115
it(`should show an error message if you enter an invalid repo in the text box: ${repo}`, async () => {
90116
// fake return values
117+
quickPickSpy.resolves(
118+
{ useCustomRepository: true }
119+
);
91120
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
92121
showInputBoxSpy.resolves(repo);
93122

94123
// make the function call
95-
await mod.getRepositories();
124+
await mod.getRepositorySelection();
96125

97126
// check that we get the right error message
98127
expect(showAndLogErrorMessageSpy.firstCall.args[0]).to.contain('Invalid repository format');

0 commit comments

Comments
 (0)