Skip to content

Commit 4f51445

Browse files
committed
Merge remote-tracking branch 'origin/main' into koesie10/download-multiple-github-databases
2 parents d0488dd + 01d24e0 commit 4f51445

5 files changed

Lines changed: 601 additions & 171 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { RequestError } from "@octokit/request-error";
2+
import { Octokit } from "@octokit/rest";
3+
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
4+
import { showNeverAskAgainDialog } from "../common/vscode/dialog";
5+
import { GitHubDatabaseConfig } from "../config";
6+
import { Credentials } from "../common/authentication";
7+
import { AppOctokit } from "../common/octokit";
8+
9+
export type CodeqlDatabase =
10+
RestEndpointMethodTypes["codeScanning"]["listCodeqlDatabases"]["response"]["data"][number];
11+
12+
/**
13+
* Ask the user if they want to connect to GitHub to download CodeQL databases.
14+
* This should be used when the user does not have an access token and should
15+
* be followed by an access token prompt.
16+
*/
17+
async function askForGitHubConnect(
18+
config: GitHubDatabaseConfig,
19+
): Promise<boolean> {
20+
const answer = await showNeverAskAgainDialog(
21+
"This repository has an origin (GitHub) that may have one or more CodeQL databases. Connect to GitHub and download any existing databases?",
22+
false,
23+
"Connect",
24+
"Not now",
25+
"Never",
26+
);
27+
28+
if (answer === "Not now" || answer === undefined) {
29+
return false;
30+
}
31+
32+
if (answer === "Never") {
33+
await config.setDownload("never");
34+
return false;
35+
}
36+
37+
return true;
38+
}
39+
40+
export type ListDatabasesResult = {
41+
/**
42+
* Whether the user has been prompted for credentials. This can be used to determine
43+
* follow-up actions based on whether the user has already had any feedback.
44+
*/
45+
promptedForCredentials: boolean;
46+
databases: CodeqlDatabase[];
47+
octokit: Octokit;
48+
};
49+
50+
/**
51+
* List CodeQL databases for a GitHub repository.
52+
*
53+
* This will first try to fetch the CodeQL databases for the repository with
54+
* existing credentials (or none if there are none). If that fails, it will
55+
* prompt the user to connect to GitHub and try again.
56+
*
57+
* If the user does not want to connect to GitHub, this will return `undefined`.
58+
*/
59+
export async function listDatabases(
60+
owner: string,
61+
repo: string,
62+
credentials: Credentials,
63+
config: GitHubDatabaseConfig,
64+
): Promise<ListDatabasesResult | undefined> {
65+
const hasAccessToken = !!(await credentials.getExistingAccessToken());
66+
67+
let octokit = hasAccessToken
68+
? await credentials.getOctokit()
69+
: new AppOctokit();
70+
71+
let promptedForCredentials = false;
72+
73+
let databases: CodeqlDatabase[];
74+
try {
75+
const response = await octokit.rest.codeScanning.listCodeqlDatabases({
76+
owner,
77+
repo,
78+
});
79+
databases = response.data;
80+
} catch (e) {
81+
// If we get a 404 when we don't have an access token, it might be because
82+
// the repository is private/internal. Therefore, we should ask the user
83+
// whether they want to connect to GitHub and try again.
84+
if (e instanceof RequestError && e.status === 404 && !hasAccessToken) {
85+
// Check whether the user wants to connect to GitHub
86+
if (!(await askForGitHubConnect(config))) {
87+
return;
88+
}
89+
90+
// Prompt for credentials
91+
octokit = await credentials.getOctokit();
92+
93+
promptedForCredentials = true;
94+
95+
const response = await octokit.rest.codeScanning.listCodeqlDatabases({
96+
owner,
97+
repo,
98+
});
99+
databases = response.data;
100+
} else {
101+
throw e;
102+
}
103+
}
104+
105+
return {
106+
promptedForCredentials,
107+
databases,
108+
octokit,
109+
};
110+
}

extensions/ql-vscode/src/databases/github-database-prompt.ts renamed to extensions/ql-vscode/src/databases/github-database-download.ts

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { window } from "vscode";
2-
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
32
import { Octokit } from "@octokit/rest";
43
import { showNeverAskAgainDialog } from "../common/vscode/dialog";
54
import { getLanguageDisplayName } from "../common/query-language";
@@ -9,71 +8,61 @@ import { DatabaseManager } from "./local-databases";
98
import { CodeQLCliServer } from "../codeql-cli/cli";
109
import { AppCommandManager } from "../common/commands";
1110
import { GitHubDatabaseConfig } from "../config";
12-
13-
export type CodeqlDatabase =
14-
RestEndpointMethodTypes["codeScanning"]["listCodeqlDatabases"]["response"]["data"][number];
15-
16-
export async function findGitHubDatabasesForRepository(
17-
octokit: Octokit,
18-
owner: string,
19-
repo: string,
20-
): Promise<CodeqlDatabase[]> {
21-
const response = await octokit.rest.codeScanning.listCodeqlDatabases({
22-
owner,
23-
repo,
24-
});
25-
26-
return response.data;
27-
}
11+
import type { CodeqlDatabase } from "./github-database-api";
2812

2913
/**
30-
* Prompt the user to download a database from GitHub. This is a blocking method, so this should
31-
* almost never be called with `await`.
14+
* Ask whether the user wants to download a database from GitHub.
15+
* @return true if the user wants to download a database, false otherwise.
3216
*/
33-
export async function promptGitHubDatabaseDownload(
34-
octokit: Octokit,
35-
owner: string,
36-
repo: string,
17+
export async function askForGitHubDatabaseDownload(
3718
databases: CodeqlDatabase[],
3819
config: GitHubDatabaseConfig,
39-
databaseManager: DatabaseManager,
40-
storagePath: string,
41-
cliServer: CodeQLCliServer,
42-
commandManager: AppCommandManager,
43-
): Promise<void> {
20+
): Promise<boolean> {
4421
const languages = databases.map((database) => database.language);
4522

46-
const databasesMessage =
23+
const message =
4724
databases.length === 1
4825
? `This repository has an origin (GitHub) that has a ${getLanguageDisplayName(
4926
languages[0],
50-
)} CodeQL database.`
27+
)} CodeQL database. Download the existing database from GitHub?`
5128
: `This repository has an origin (GitHub) that has ${joinLanguages(
5229
languages,
53-
)} CodeQL databases.`;
54-
55-
const connectMessage =
56-
databases.length === 1
57-
? `Connect to GitHub and download the existing database?`
58-
: `Connect to GitHub and download any existing databases?`;
30+
)} CodeQL databases. Download any existing databases from GitHub?`;
5931

6032
const answer = await showNeverAskAgainDialog(
61-
`${databasesMessage} ${connectMessage}`,
33+
message,
6234
false,
63-
"Connect",
35+
"Download",
6436
"Not now",
6537
"Never",
6638
);
6739

6840
if (answer === "Not now" || answer === undefined) {
69-
return;
41+
return false;
7042
}
7143

7244
if (answer === "Never") {
7345
await config.setDownload("never");
74-
return;
46+
return false;
7547
}
7648

49+
return true;
50+
}
51+
52+
/**
53+
* Download a database from GitHub by asking the user for a language and then
54+
* downloading the database for that language.
55+
*/
56+
export async function downloadDatabaseFromGitHub(
57+
octokit: Octokit,
58+
owner: string,
59+
repo: string,
60+
databases: CodeqlDatabase[],
61+
databaseManager: DatabaseManager,
62+
storagePath: string,
63+
cliServer: CodeQLCliServer,
64+
commandManager: AppCommandManager,
65+
): Promise<void> {
7766
const selectedDatabases = await promptForDatabases(databases);
7867
if (selectedDatabases.length === 0) {
7968
return;

extensions/ql-vscode/src/databases/github-database-module.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1+
import { window } from "vscode";
12
import { DisposableObject } from "../common/disposable-object";
23
import { App } from "../common/app";
34
import { findGitHubRepositoryForWorkspace } from "./github-repository-finder";
45
import { redactableError } from "../common/errors";
56
import { asError, getErrorMessage } from "../common/helpers-pure";
67
import {
7-
CodeqlDatabase,
8-
findGitHubDatabasesForRepository,
9-
promptGitHubDatabaseDownload,
10-
} from "./github-database-prompt";
11-
import {
12-
GitHubDatabaseConfig,
13-
GitHubDatabaseConfigListener,
14-
isCanary,
15-
} from "../config";
16-
import { AppOctokit } from "../common/octokit";
8+
askForGitHubDatabaseDownload,
9+
downloadDatabaseFromGitHub,
10+
} from "./github-database-download";
11+
import { GitHubDatabaseConfig, GitHubDatabaseConfigListener } from "../config";
1712
import { DatabaseManager } from "./local-databases";
1813
import { CodeQLCliServer } from "../codeql-cli/cli";
14+
import { listDatabases, ListDatabasesResult } from "./github-database-api";
1915

2016
export class GithubDatabaseModule extends DisposableObject {
2117
private readonly config: GitHubDatabaseConfig;
@@ -93,18 +89,13 @@ export class GithubDatabaseModule extends DisposableObject {
9389
return;
9490
}
9591

96-
const credentials = isCanary() ? this.app.credentials : undefined;
97-
98-
const octokit = credentials
99-
? await credentials.getOctokit()
100-
: new AppOctokit();
101-
102-
let databases: CodeqlDatabase[];
92+
let result: ListDatabasesResult | undefined;
10393
try {
104-
databases = await findGitHubDatabasesForRepository(
105-
octokit,
94+
result = await listDatabases(
10695
githubRepository.owner,
10796
githubRepository.name,
97+
this.app.credentials,
98+
this.config,
10899
);
109100
} catch (e) {
110101
this.app.telemetry?.sendError(
@@ -120,16 +111,37 @@ export class GithubDatabaseModule extends DisposableObject {
120111
return;
121112
}
122113

114+
// This means the user didn't want to connect, so we can just return.
115+
if (result === undefined) {
116+
return;
117+
}
118+
119+
const { databases, promptedForCredentials, octokit } = result;
120+
123121
if (databases.length === 0) {
122+
// If the user didn't have an access token, they have already been prompted,
123+
// so we should give feedback.
124+
if (promptedForCredentials) {
125+
void window.showInformationMessage(
126+
"The GitHub repository does not have any CodeQL databases.",
127+
);
128+
}
129+
124130
return;
125131
}
126132

127-
void promptGitHubDatabaseDownload(
133+
// If the user already had an access token, first ask if they even want to download the DB.
134+
if (!promptedForCredentials) {
135+
if (!(await askForGitHubDatabaseDownload(databases, this.config))) {
136+
return;
137+
}
138+
}
139+
140+
await downloadDatabaseFromGitHub(
128141
octokit,
129142
githubRepository.owner,
130143
githubRepository.name,
131144
databases,
132-
this.config,
133145
this.databaseManager,
134146
this.databaseStoragePath,
135147
this.cliServer,

0 commit comments

Comments
 (0)