Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,10 +579,6 @@ export function isVariantAnalysisLiveResultsEnabled(): boolean {
return true;
}

export function isVariantAnalysisReposPanelEnabled(): boolean {
return true;
}

// Settings for mocking the GitHub API.
const MOCK_GH_API_SERVER = new Setting("mockGitHubApiServer", ROOT_SETTING);

Expand Down
4 changes: 2 additions & 2 deletions extensions/ql-vscode/src/databases/db-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel";
import { DbSelectionDecorationProvider } from "./ui/db-selection-decoration-provider";
import { isCanary, isVariantAnalysisReposPanelEnabled } from "../config";
import { isCanary } from "../config";

export class DbModule extends DisposableObject {
public readonly dbManager: DbManager;
Expand Down Expand Up @@ -36,7 +36,7 @@ export class DbModule extends DisposableObject {
return true;
}

return isCanary() && isVariantAnalysisReposPanelEnabled();
return isCanary();
}

private async initialize(app: App): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ async function activateWithInstalledDistribution(
cliServer,
variantAnalysisStorageDir,
variantAnalysisResultsManager,
dbModule?.dbManager, // the dbModule is only needed when variantAnalysisReposPanel is enabled
dbModule?.dbManager,
);
ctx.subscriptions.push(variantAnalysisManager);
ctx.subscriptions.push(variantAnalysisResultsManager);
Expand Down
263 changes: 23 additions & 240 deletions extensions/ql-vscode/src/variant-analysis/repository-selection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
import { pathExists as fs_pathExists, stat, readFile } from "fs-extra";
import { QuickPickItem, window } from "vscode";
import { extLogger } from "../common";
import {
getRemoteRepositoryLists,
getRemoteRepositoryListsPath,
isVariantAnalysisReposPanelEnabled,
} from "../config";
import { OWNER_REGEX, REPO_REGEX } from "../pure/helpers-pure";
import { UserCancellationException } from "../commandRunner";
import { DbManager } from "../databases/db-manager";
import { DbItemKind } from "../databases/db-item";
Expand All @@ -17,120 +8,42 @@ export interface RepositorySelection {
owners?: string[];
}

interface RepoListQuickPickItem extends QuickPickItem {
repositories?: string[];
repositoryList?: string;
useCustomRepo?: boolean;
useAllReposOfOwner?: boolean;
}

interface RepoList {
label: string;
repositories: string[];
}

/**
* Gets the repositories or repository lists to run the query against.
* @returns The user selection.
*/
export async function getRepositorySelection(
dbManager?: DbManager,
): Promise<RepositorySelection> {
if (isVariantAnalysisReposPanelEnabled()) {
const selectedDbItem = dbManager?.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
const selectedDbItem = dbManager?.getSelectedDbItem();
if (selectedDbItem) {
switch (selectedDbItem.kind) {
case DbItemKind.LocalDatabase || DbItemKind.LocalList:
throw new UserCancellationException(
"Local databases and lists are not supported yet.",
);
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:
if (selectedDbItem.repos.length === 0) {
throw new UserCancellationException(
"Local databases and lists are not supported yet.",
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
case DbItemKind.RemoteSystemDefinedList:
return { repositoryLists: [selectedDbItem.listName] };
case DbItemKind.RemoteUserDefinedList:
if (selectedDbItem.repos.length === 0) {
throw new UserCancellationException(
"The selected repository list is empty. Please add repositories to it before running a variant analysis.",
);
} else {
return {
repositories: selectedDbItem.repos.map(
(repo) => repo.repoFullName,
),
};
}
case DbItemKind.RemoteOwner:
return { owners: [selectedDbItem.ownerName] };
case DbItemKind.RemoteRepo:
return { repositories: [selectedDbItem.repoFullName] };
}
} else {
throw new UserCancellationException(
"Please select a remote database to run the query against.",
);
} else {
return {
repositories: selectedDbItem.repos.map((repo) => repo.repoFullName),
};
}
case DbItemKind.RemoteOwner:
return { owners: [selectedDbItem.ownerName] };
case DbItemKind.RemoteRepo:
return { repositories: [selectedDbItem.repoFullName] };
}
}

const quickPickItems = [
createCustomRepoQuickPickItem(),
createAllReposOfOwnerQuickPickItem(),
...createSystemDefinedRepoListsQuickPickItems(),
...(await createUserDefinedRepoListsQuickPickItems()),
];

const options = {
placeHolder:
"Select a repository list. You can define repository lists in the `codeQL.variantAnalysis.repositoryLists` setting.",
ignoreFocusOut: true,
};

const quickpick = await window.showQuickPick<RepoListQuickPickItem>(
quickPickItems,
options,
throw new UserCancellationException(
"Please select a remote database to run the query against.",
);

if (!quickpick) {
// We don't need to display a warning pop-up in this case, since the user just escaped out of the operation.
// We set 'true' to make this a silent exception.
throw new UserCancellationException("No repositories selected", true);
}

if (quickpick.repositories?.length) {
void extLogger.log(
`Selected repositories: ${quickpick.repositories.join(", ")}`,
);
return { repositories: quickpick.repositories };
} else if (quickpick.repositoryList) {
void extLogger.log(`Selected repository list: ${quickpick.repositoryList}`);
return { repositoryLists: [quickpick.repositoryList] };
} else if (quickpick.useCustomRepo) {
const customRepo = await getCustomRepo();
if (customRepo === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException("No repositories selected", true);
}
if (!customRepo || !REPO_REGEX.test(customRepo)) {
throw new UserCancellationException(
"Invalid repository format. Please enter a valid repository in the format <owner>/<repo> (e.g. github/codeql)",
);
}
void extLogger.log(`Entered repository: ${customRepo}`);
return { repositories: [customRepo] };
} else if (quickpick.useAllReposOfOwner) {
const owner = await getOwner();
if (owner === undefined) {
// The user cancelled, do nothing.
throw new UserCancellationException("No repositories selected", true);
}
if (!owner || !OWNER_REGEX.test(owner)) {
throw new Error(`Invalid user or organization: ${owner}`);
}
void extLogger.log(`Entered owner: ${owner}`);
return { owners: [owner] };
} else {
// This means the user has selected something, but there is nothing actually linked to this item. We want to show
// this to the user.
throw new UserCancellationException("No repositories selected", false);
}
}

/**
Expand All @@ -147,133 +60,3 @@ export function isValidSelection(repoSelection: RepositorySelection): boolean {
repositories.length > 0 || repositoryLists.length > 0 || owners.length > 0
);
}

function createSystemDefinedRepoListsQuickPickItems(): RepoListQuickPickItem[] {
const topNs = [10, 100, 1000];

return topNs.map(
(n) =>
({
label: `$(star) Top ${n}`,
repositoryList: `top_${n}`,
alwaysShow: true,
} as RepoListQuickPickItem),
);
}

async function readExternalRepoLists(): Promise<RepoList[]> {
const repoLists: RepoList[] = [];

const path = getRemoteRepositoryListsPath();
if (!path) {
return repoLists;
}

await validateExternalRepoListsFile(path);
const json = await readExternalRepoListsJson(path);

for (const [repoListName, repositories] of Object.entries(json)) {
if (!Array.isArray(repositories)) {
throw Error(
"Invalid repository lists file. It should contain an array of repositories for each list.",
);
}

repoLists.push({
label: repoListName,
repositories,
});
}

return repoLists;
}

async function validateExternalRepoListsFile(path: string): Promise<void> {
const pathExists = await fs_pathExists(path);
if (!pathExists) {
throw Error(`External repository lists file does not exist at ${path}`);
}

const pathStat = await stat(path);
if (pathStat.isDirectory()) {
throw Error(
"External repository lists path should not point to a directory",
);
}
}

async function readExternalRepoListsJson(
path: string,
): Promise<Record<string, unknown>> {
let json;

try {
const fileContents = await readFile(path, "utf8");
json = await JSON.parse(fileContents);
} catch (error) {
throw Error("Invalid repository lists file. It should contain valid JSON.");
}

if (Array.isArray(json)) {
throw Error(
"Invalid repository lists file. It should be an object mapping names to a list of repositories.",
);
}

return json;
}

function readRepoListsFromSettings(): RepoList[] {
const repoLists = getRemoteRepositoryLists();
if (!repoLists) {
return [];
}

return Object.entries(repoLists).map<RepoList>(([label, repositories]) => ({
label,
repositories,
}));
}

async function createUserDefinedRepoListsQuickPickItems(): Promise<
RepoListQuickPickItem[]
> {
const repoListsFromSetings = readRepoListsFromSettings();
const repoListsFromExternalFile = await readExternalRepoLists();

return [...repoListsFromSetings, ...repoListsFromExternalFile];
}

function createCustomRepoQuickPickItem(): RepoListQuickPickItem {
return {
label: "$(edit) Enter a GitHub repository",
useCustomRepo: true,
alwaysShow: true,
};
}

function createAllReposOfOwnerQuickPickItem(): RepoListQuickPickItem {
return {
label: "$(edit) Enter a GitHub user or organization",
useAllReposOfOwner: true,
alwaysShow: true,
};
}

async function getCustomRepo(): Promise<string | undefined> {
return await window.showInputBox({
title:
"Enter a GitHub repository in the format <owner>/<repo> (e.g. github/codeql)",
placeHolder: "<owner>/<repo>",
prompt:
"Tip: you can save frequently used repositories in the `codeQL.variantAnalysis.repositoryLists` setting",
ignoreFocusOut: true,
});
}

async function getOwner(): Promise<string | undefined> {
return await window.showInputBox({
title: "Enter a GitHub user or organization",
ignoreFocusOut: true,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export async function prepareRemoteQueryRun(
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken,
dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
dbManager?: DbManager,
): Promise<PreparedRemoteQuery> {
if (!uri?.fsPath.endsWith(".ql")) {
throw new UserCancellationException("Not a CodeQL query file.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import {
} from "../pure/variant-analysis-filter-sort";
import { URLSearchParams } from "url";
import { DbManager } from "../databases/db-manager";
import { isVariantAnalysisReposPanelEnabled } from "../config";
import { App } from "../common/app";
import { redactableError } from "../pure/errors";

Expand Down Expand Up @@ -106,7 +105,7 @@ export class VariantAnalysisManager
private readonly cliServer: CodeQLCliServer,
private readonly storagePath: string,
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
private readonly dbManager?: DbManager, // the dbManager is only needed when variantAnalysisReposPanel is enabled
private readonly dbManager?: DbManager,
) {
super();
this.variantAnalysisMonitor = this.push(
Expand Down Expand Up @@ -635,25 +634,15 @@ export class VariantAnalysisManager
return;
}

let text: string[];
if (isVariantAnalysisReposPanelEnabled()) {
text = [
"{",
` "name": "new-repo-list",`,
` "repositories": [`,
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
` ]`,
"}",
];
} else {
text = [
'"new-repo-list": [',
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
"]",
];
}
const text = [
"{",
` "name": "new-repo-list",`,
` "repositories": [`,
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
` ]`,
"}",
];

await env.clipboard.writeText(text.join(EOL));
}
Expand Down
Loading