Skip to content

Commit 0ebff2d

Browse files
authored
Add ability of running MRVA against a whole org (#1372)
1 parent d061634 commit 0ebff2d

File tree

4 files changed

+103
-27
lines changed

4 files changed

+103
-27
lines changed

extensions/ql-vscode/src/pure/helpers-pure.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export const asyncFilter = async function <T>(arr: T[], predicate: (arg0: T) =>
3838
*/
3939
export const REPO_REGEX = /^[a-zA-Z0-9-_\.]+\/[a-zA-Z0-9-_\.]+$/;
4040

41+
/**
42+
* This regex matches GiHub organization and user strings. These are made up for alphanumeric
43+
* characters, hyphens, underscores or periods.
44+
*/
45+
export const OWNER_REGEX = /^[a-zA-Z0-9-_\.]+$/;
46+
4147
export function getErrorMessage(e: any) {
4248
return e instanceof Error ? e.message : String(e);
4349
}

extensions/ql-vscode/src/remote-queries/repository-selection.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { QuickPickItem, window } from 'vscode';
22
import { logger } from '../logging';
33
import { getRemoteRepositoryLists } from '../config';
4-
import { REPO_REGEX } from '../pure/helpers-pure';
4+
import { OWNER_REGEX, REPO_REGEX } from '../pure/helpers-pure';
55
import { UserCancellationException } from '../commandRunner';
66

77
export interface RepositorySelection {
88
repositories?: string[];
9-
repositoryLists?: string[]
9+
repositoryLists?: string[];
10+
owners?: string[];
1011
}
1112

1213
interface RepoListQuickPickItem extends QuickPickItem {
1314
repositories?: string[];
1415
repositoryList?: string;
15-
useCustomRepository?: boolean;
16+
useCustomRepo?: boolean;
17+
useAllReposOfOwner?: boolean;
1618
}
1719

1820
/**
@@ -22,6 +24,7 @@ interface RepoListQuickPickItem extends QuickPickItem {
2224
export async function getRepositorySelection(): Promise<RepositorySelection> {
2325
const quickPickItems = [
2426
createCustomRepoQuickPickItem(),
27+
createAllReposOfOwnerQuickPickItem(),
2528
...createSystemDefinedRepoListsQuickPickItems(),
2629
...createUserDefinedRepoListsQuickPickItems(),
2730
];
@@ -41,13 +44,20 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
4144
} else if (quickpick?.repositoryList) {
4245
void logger.log(`Selected repository list: ${quickpick.repositoryList}`);
4346
return { repositoryLists: [quickpick.repositoryList] };
44-
} else if (quickpick?.useCustomRepository) {
47+
} else if (quickpick?.useCustomRepo) {
4548
const customRepo = await getCustomRepo();
4649
if (!customRepo || !REPO_REGEX.test(customRepo)) {
4750
throw new UserCancellationException('Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)');
4851
}
4952
void logger.log(`Entered repository: ${customRepo}`);
5053
return { repositories: [customRepo] };
54+
} else if (quickpick?.useAllReposOfOwner) {
55+
const owner = await getOwner();
56+
if (!owner || !OWNER_REGEX.test(owner)) {
57+
throw new Error(`Invalid user or organization: ${owner}`);
58+
}
59+
void logger.log(`Entered owner: ${owner}`);
60+
return { owners: [owner] };
5161
} else {
5262
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
5363
// We set 'true' to make this a silent exception.
@@ -61,17 +71,11 @@ export async function getRepositorySelection(): Promise<RepositorySelection> {
6171
* @returns A boolean flag indicating if the selection is valid or not.
6272
*/
6373
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;
69-
}
70-
if (repoSelection.repositoryLists?.length === 0) {
71-
return false;
72-
}
74+
const repositories = repoSelection.repositories || [];
75+
const repositoryLists = repoSelection.repositoryLists || [];
76+
const owners = repoSelection.owners || [];
7377

74-
return true;
78+
return (repositories.length > 0 || repositoryLists.length > 0 || owners.length > 0);
7579
}
7680

7781
function createSystemDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
@@ -101,11 +105,19 @@ function createUserDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
101105
function createCustomRepoQuickPickItem(): RepoListQuickPickItem {
102106
return {
103107
label: '$(edit) Enter a GitHub repository',
104-
useCustomRepository: true,
108+
useCustomRepo: true,
105109
alwaysShow: true,
106110
};
107111
}
108112

113+
function createAllReposOfOwnerQuickPickItem(): RepoListQuickPickItem {
114+
return {
115+
label: '$(edit) Enter a GitHub user or organization',
116+
useAllReposOfOwner: true,
117+
alwaysShow: true
118+
};
119+
}
120+
109121
async function getCustomRepo(): Promise<string | undefined> {
110122
return await window.showInputBox({
111123
title: 'Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)',
@@ -114,3 +126,10 @@ async function getCustomRepo(): Promise<string | undefined> {
114126
ignoreFocusOut: true,
115127
});
116128
}
129+
130+
async function getOwner(): Promise<string | undefined> {
131+
return await window.showInputBox({
132+
title: 'Enter a GitHub user or organization',
133+
ignoreFocusOut: true,
134+
});
135+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ async function runRemoteQueriesApiRequest(
304304
language,
305305
repositories: repoSelection.repositories ?? undefined,
306306
repository_lists: repoSelection.repositoryLists ?? undefined,
307+
repository_owners: repoSelection.owners ?? undefined,
307308
query_pack: queryPackBase64,
308309
};
309310

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('repository-selection', function() {
3131
});
3232

3333
it('should allow selection from repo lists from your pre-defined config', async () => {
34-
// fake return values
34+
// Fake return values
3535
quickPickSpy.resolves(
3636
{ repositories: ['foo/bar', 'foo/baz'] }
3737
);
@@ -42,18 +42,19 @@ describe('repository-selection', function() {
4242
}
4343
);
4444

45-
// make the function call
45+
// Make the function call
4646
const repoSelection = await mod.getRepositorySelection();
4747

4848
// Check that the return value is correct
4949
expect(repoSelection.repositoryLists).to.be.undefined;
50+
expect(repoSelection.owners).to.be.undefined;
5051
expect(repoSelection.repositories).to.deep.eq(
5152
['foo/bar', 'foo/baz']
5253
);
5354
});
5455

5556
it('should allow selection from repo lists defined at the system level', async () => {
56-
// fake return values
57+
// Fake return values
5758
quickPickSpy.resolves(
5859
{ repositoryList: 'top_100' }
5960
);
@@ -64,42 +65,91 @@ describe('repository-selection', function() {
6465
}
6566
);
6667

67-
// make the function call
68+
// Make the function call
6869
const repoSelection = await mod.getRepositorySelection();
6970

7071
// Check that the return value is correct
7172
expect(repoSelection.repositories).to.be.undefined;
73+
expect(repoSelection.owners).to.be.undefined;
7274
expect(repoSelection.repositoryLists).to.deep.eq(
7375
['top_100']
7476
);
7577
});
7678

77-
// Test the regex in various "good" cases
79+
// Test the owner regex in various "good" cases
80+
const goodOwners = [
81+
'owner',
82+
'owner-with-hyphens',
83+
'ownerWithNumbers58',
84+
'owner_with_underscores',
85+
'owner.with.periods.'
86+
];
87+
goodOwners.forEach(owner => {
88+
it(`should run on a valid owner that you enter in the text box: ${owner}`, async () => {
89+
// Fake return values
90+
quickPickSpy.resolves(
91+
{ useAllReposOfOwner: true }
92+
);
93+
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
94+
showInputBoxSpy.resolves(owner);
95+
96+
// Make the function call
97+
const repoSelection = await mod.getRepositorySelection();
98+
99+
// Check that the return value is correct
100+
expect(repoSelection.repositories).to.be.undefined;
101+
expect(repoSelection.repositoryLists).to.be.undefined;
102+
expect(repoSelection.owners).to.deep.eq([owner]);
103+
});
104+
});
105+
106+
// Test the owner regex in various "bad" cases
107+
const badOwners = [
108+
'invalid&owner',
109+
'owner-with-repo/repo'
110+
];
111+
badOwners.forEach(owner => {
112+
it(`should show an error message if you enter an invalid owner in the text box: ${owner}`, async () => {
113+
// Fake return values
114+
quickPickSpy.resolves(
115+
{ useAllReposOfOwner: true }
116+
);
117+
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
118+
showInputBoxSpy.resolves(owner);
119+
120+
// Function call should throw a UserCancellationException
121+
await expect(mod.getRepositorySelection()).to.be.rejectedWith(Error, `Invalid user or organization: ${owner}`);
122+
});
123+
});
124+
125+
// Test the repo regex in various "good" cases
78126
const goodRepos = [
79127
'owner/repo',
80128
'owner_with.symbols-/repo.with-symbols_',
81129
'ownerWithNumbers58/repoWithNumbers37'
82130
];
83131
goodRepos.forEach(repo => {
84132
it(`should run on a valid repo that you enter in the text box: ${repo}`, async () => {
85-
// fake return values
133+
// Fake return values
86134
quickPickSpy.resolves(
87-
{ useCustomRepository: true }
135+
{ useCustomRepo: true }
88136
);
89137
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
90138
showInputBoxSpy.resolves(repo);
91139

92-
// make the function call
140+
// Make the function call
93141
const repoSelection = await mod.getRepositorySelection();
94142

95143
// Check that the return value is correct
144+
expect(repoSelection.repositoryLists).to.be.undefined;
145+
expect(repoSelection.owners).to.be.undefined;
96146
expect(repoSelection.repositories).to.deep.equal(
97147
[repo]
98148
);
99149
});
100150
});
101151

102-
// Test the regex in various "bad" cases
152+
// Test the repo regex in various "bad" cases
103153
const badRepos = [
104154
'invalid*owner/repo',
105155
'owner/repo+some&invalid&stuff',
@@ -108,14 +158,14 @@ describe('repository-selection', function() {
108158
];
109159
badRepos.forEach(repo => {
110160
it(`should show an error message if you enter an invalid repo in the text box: ${repo}`, async () => {
111-
// fake return values
161+
// Fake return values
112162
quickPickSpy.resolves(
113-
{ useCustomRepository: true }
163+
{ useCustomRepo: true }
114164
);
115165
getRemoteRepositoryListsSpy.returns({}); // no pre-defined repo lists
116166
showInputBoxSpy.resolves(repo);
117167

118-
// function call should throw a UserCancellationException
168+
// Function call should throw a UserCancellationException
119169
await expect(mod.getRepositorySelection()).to.be.rejectedWith(UserCancellationException, 'Invalid repository format');
120170
});
121171
});

0 commit comments

Comments
 (0)