Skip to content

Commit a0295d6

Browse files
committed
Add prompt for updating GitHub databases
1 parent 4f51445 commit a0295d6

6 files changed

Lines changed: 858 additions & 15 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,19 @@
441441
"Never download a GitHub databases when a workspace is opened."
442442
],
443443
"description": "Ask to download a GitHub database when a workspace is opened."
444+
},
445+
"codeQL.githubDatabase.update": {
446+
"type": "string",
447+
"default": "ask",
448+
"enum": [
449+
"ask",
450+
"never"
451+
],
452+
"enumDescriptions": [
453+
"Ask to download an updated GitHub database when a new version is available.",
454+
"Never download an updated GitHub database when a new version is available."
455+
],
456+
"description": "Ask to download an updated GitHub database when a new version is available."
444457
}
445458
}
446459
},

extensions/ql-vscode/src/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,13 +788,23 @@ const GITHUB_DATABASE_DOWNLOAD = new Setting(
788788
const GitHubDatabaseDownloadValues = ["ask", "never"] as const;
789789
type GitHubDatabaseDownload = (typeof GitHubDatabaseDownloadValues)[number];
790790

791+
const GITHUB_DATABASE_UPDATE = new Setting("update", GITHUB_DATABASE_SETTING);
792+
793+
const GitHubDatabaseUpdateValues = ["ask", "never"] as const;
794+
type GitHubDatabaseUpdate = (typeof GitHubDatabaseUpdateValues)[number];
795+
791796
export interface GitHubDatabaseConfig {
792797
enable: boolean;
793798
download: GitHubDatabaseDownload;
799+
update: GitHubDatabaseUpdate;
794800
setDownload(
795801
value: GitHubDatabaseDownload,
796802
target?: ConfigurationTarget,
797803
): Promise<void>;
804+
setUpdate(
805+
value: GitHubDatabaseUpdate,
806+
target?: ConfigurationTarget,
807+
): Promise<void>;
798808
}
799809

800810
export class GitHubDatabaseConfigListener
@@ -817,10 +827,22 @@ export class GitHubDatabaseConfigListener
817827
return GitHubDatabaseDownloadValues.includes(value) ? value : "ask";
818828
}
819829

830+
public get update(): GitHubDatabaseUpdate {
831+
const value = GITHUB_DATABASE_UPDATE.getValue<GitHubDatabaseUpdate>();
832+
return GitHubDatabaseUpdateValues.includes(value) ? value : "ask";
833+
}
834+
820835
public async setDownload(
821836
value: GitHubDatabaseDownload,
822837
target: ConfigurationTarget = ConfigurationTarget.Workspace,
823838
): Promise<void> {
824839
await GITHUB_DATABASE_DOWNLOAD.updateValue(value, target);
825840
}
841+
842+
public async setUpdate(
843+
value: GitHubDatabaseUpdate,
844+
target: ConfigurationTarget = ConfigurationTarget.Workspace,
845+
): Promise<void> {
846+
await GITHUB_DATABASE_UPDATE.updateValue(value, target);
847+
}
826848
}

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export async function downloadDatabaseFromGitHub(
110110
*
111111
* @param languages The languages to join. These should be language identifiers, such as `csharp`.
112112
*/
113-
function joinLanguages(languages: string[]): string {
113+
export function joinLanguages(languages: string[]): string {
114114
const languageDisplayNames = languages
115115
.map((language) => getLanguageDisplayName(language))
116116
.sort();
@@ -130,8 +130,17 @@ function joinLanguages(languages: string[]): string {
130130
return result;
131131
}
132132

133-
async function promptForDatabases(
133+
type PromptForDatabasesOptions = {
134+
title?: string;
135+
placeHolder?: string;
136+
};
137+
138+
export async function promptForDatabases(
134139
databases: CodeqlDatabase[],
140+
{
141+
title = "Select databases to download",
142+
placeHolder = "Databases found in this repository",
143+
}: PromptForDatabasesOptions = {},
135144
): Promise<CodeqlDatabase[]> {
136145
if (databases.length === 1) {
137146
return databases;
@@ -152,8 +161,8 @@ async function promptForDatabases(
152161
.sort((a, b) => a.label.localeCompare(b.label));
153162

154163
const selectedItems = await window.showQuickPick(items, {
155-
title: "Select databases to download",
156-
placeHolder: "Databases found in this repository",
164+
title,
165+
placeHolder,
157166
ignoreFocusOut: true,
158167
canPickMany: true,
159168
});

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DisposableObject } from "../common/disposable-object";
33
import { App } from "../common/app";
44
import { findGitHubRepositoryForWorkspace } from "./github-repository-finder";
55
import { redactableError } from "../common/errors";
6-
import { asError, getErrorMessage } from "../common/helpers-pure";
6+
import { asError, assertNever, getErrorMessage } from "../common/helpers-pure";
77
import {
88
askForGitHubDatabaseDownload,
99
downloadDatabaseFromGitHub,
@@ -12,6 +12,11 @@ import { GitHubDatabaseConfig, GitHubDatabaseConfigListener } from "../config";
1212
import { DatabaseManager } from "./local-databases";
1313
import { CodeQLCliServer } from "../codeql-cli/cli";
1414
import { listDatabases, ListDatabasesResult } from "./github-database-api";
15+
import {
16+
askForGitHubDatabaseUpdate,
17+
downloadDatabaseUpdateFromGitHub,
18+
isNewerDatabaseAvailable,
19+
} from "./github-database-updates";
1520

1621
export class GithubDatabaseModule extends DisposableObject {
1722
private readonly config: GitHubDatabaseConfig;
@@ -79,16 +84,6 @@ export class GithubDatabaseModule extends DisposableObject {
7984

8085
const githubRepository = githubRepositoryResult.value;
8186

82-
const hasExistingDatabase = this.databaseManager.databaseItems.some(
83-
(db) =>
84-
db.origin?.type === "github" &&
85-
db.origin.repository ===
86-
`${githubRepository.owner}/${githubRepository.name}`,
87-
);
88-
if (hasExistingDatabase) {
89-
return;
90-
}
91-
9287
let result: ListDatabasesResult | undefined;
9388
try {
9489
result = await listDatabases(
@@ -130,6 +125,48 @@ export class GithubDatabaseModule extends DisposableObject {
130125
return;
131126
}
132127

128+
const updateStatus = isNewerDatabaseAvailable(
129+
databases,
130+
githubRepository.owner,
131+
githubRepository.name,
132+
this.databaseManager,
133+
);
134+
if (updateStatus.type === "upToDate") {
135+
return;
136+
}
137+
138+
if (updateStatus.type === "updateAvailable") {
139+
if (this.config.update === "never") {
140+
return;
141+
}
142+
143+
if (
144+
!(await askForGitHubDatabaseUpdate(
145+
updateStatus.databaseUpdates,
146+
this.config,
147+
))
148+
) {
149+
return;
150+
}
151+
152+
await downloadDatabaseUpdateFromGitHub(
153+
octokit,
154+
githubRepository.owner,
155+
githubRepository.name,
156+
updateStatus.databaseUpdates,
157+
this.databaseManager,
158+
this.databaseStoragePath,
159+
this.cliServer,
160+
this.app.commands,
161+
);
162+
163+
return;
164+
}
165+
166+
if (updateStatus.type !== "noDatabase") {
167+
assertNever(updateStatus);
168+
}
169+
133170
// If the user already had an access token, first ask if they even want to download the DB.
134171
if (!promptedForCredentials) {
135172
if (!(await askForGitHubDatabaseDownload(databases, this.config))) {
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { CodeqlDatabase } from "./github-database-api";
2+
import { DatabaseItem, DatabaseManager } from "./local-databases";
3+
import { Octokit } from "@octokit/rest";
4+
import { CodeQLCliServer } from "../codeql-cli/cli";
5+
import { AppCommandManager } from "../common/commands";
6+
import { getLanguageDisplayName } from "../common/query-language";
7+
import { showNeverAskAgainDialog } from "../common/vscode/dialog";
8+
import { downloadGitHubDatabaseFromUrl } from "./database-fetcher";
9+
import { withProgress } from "../common/vscode/progress";
10+
import { window } from "vscode";
11+
import { GitHubDatabaseConfig } from "../config";
12+
import { joinLanguages, promptForDatabases } from "./github-database-download";
13+
14+
export type DatabaseUpdate = {
15+
database: CodeqlDatabase;
16+
databaseItem: DatabaseItem;
17+
};
18+
19+
type DatabaseUpdateStatusUpdateAvailable = {
20+
type: "updateAvailable";
21+
databaseUpdates: DatabaseUpdate[];
22+
};
23+
24+
type DatabaseUpdateStatusUpToDate = {
25+
type: "upToDate";
26+
};
27+
28+
type DatabaseUpdateStatusNoDatabase = {
29+
type: "noDatabase";
30+
};
31+
32+
type DatabaseUpdateStatus =
33+
| DatabaseUpdateStatusUpdateAvailable
34+
| DatabaseUpdateStatusUpToDate
35+
| DatabaseUpdateStatusNoDatabase;
36+
37+
export function isNewerDatabaseAvailable(
38+
databases: CodeqlDatabase[],
39+
owner: string,
40+
name: string,
41+
databaseManager: DatabaseManager,
42+
): DatabaseUpdateStatus {
43+
// Sorted by date added ascending
44+
const existingDatabasesForRepository = databaseManager.databaseItems
45+
.filter(
46+
(db) =>
47+
db.origin?.type === "github" &&
48+
db.origin.repository === `${owner}/${name}`,
49+
)
50+
.sort((a, b) => (a.dateAdded ?? 0) - (b.dateAdded ?? 0));
51+
52+
if (existingDatabasesForRepository.length === 0) {
53+
return {
54+
type: "noDatabase",
55+
};
56+
}
57+
58+
// Sort order is guaranteed by the sort call above. The newest database is the last one.
59+
const newestExistingDatabasesByLanguage = new Map<string, DatabaseItem>();
60+
for (const existingDatabase of existingDatabasesForRepository) {
61+
newestExistingDatabasesByLanguage.set(
62+
existingDatabase.language,
63+
existingDatabase,
64+
);
65+
}
66+
67+
const databaseUpdates = Array.from(newestExistingDatabasesByLanguage.values())
68+
.map((newestExistingDatabase): DatabaseUpdate | null => {
69+
const origin = newestExistingDatabase.origin;
70+
if (origin?.type !== "github") {
71+
return null;
72+
}
73+
74+
const matchingDatabase = databases.find(
75+
(db) => db.language === newestExistingDatabase.language,
76+
);
77+
if (!matchingDatabase) {
78+
return null;
79+
}
80+
81+
if (matchingDatabase.commit_oid === origin.commitOid) {
82+
return null;
83+
}
84+
85+
return {
86+
database: matchingDatabase,
87+
databaseItem: newestExistingDatabase,
88+
};
89+
})
90+
.filter((update): update is DatabaseUpdate => update !== null)
91+
.sort((a, b) => a.database.language.localeCompare(b.database.language));
92+
93+
if (databaseUpdates.length === 0) {
94+
return {
95+
type: "upToDate",
96+
};
97+
}
98+
99+
return {
100+
type: "updateAvailable",
101+
databaseUpdates,
102+
};
103+
}
104+
105+
export async function askForGitHubDatabaseUpdate(
106+
updates: DatabaseUpdate[],
107+
config: GitHubDatabaseConfig,
108+
): Promise<boolean> {
109+
const languages = updates.map((update) => update.database.language);
110+
111+
const message =
112+
updates.length === 1
113+
? `There is a newer ${getLanguageDisplayName(
114+
languages[0],
115+
)} CodeQL database available for this repository. Download the database update from GitHub?`
116+
: `There are newer ${joinLanguages(
117+
languages,
118+
)} CodeQL databases available for this repository. Download the database updates from GitHub?`;
119+
120+
const answer = await showNeverAskAgainDialog(
121+
message,
122+
false,
123+
"Download",
124+
"Not now",
125+
"Never",
126+
);
127+
128+
if (answer === "Not now" || answer === undefined) {
129+
return false;
130+
}
131+
132+
if (answer === "Never") {
133+
await config.setUpdate("never");
134+
return false;
135+
}
136+
137+
return true;
138+
}
139+
140+
export async function downloadDatabaseUpdateFromGitHub(
141+
octokit: Octokit,
142+
owner: string,
143+
repo: string,
144+
updates: DatabaseUpdate[],
145+
databaseManager: DatabaseManager,
146+
storagePath: string,
147+
cliServer: CodeQLCliServer,
148+
commandManager: AppCommandManager,
149+
): Promise<void> {
150+
const selectedDatabases = await promptForDatabases(
151+
updates.map((update) => update.database),
152+
{
153+
title: "Select databases to update",
154+
},
155+
);
156+
if (selectedDatabases.length === 0) {
157+
return;
158+
}
159+
160+
await Promise.all(
161+
selectedDatabases.map((database) => {
162+
const update = updates.find((update) => update.database === database);
163+
if (!update) {
164+
return;
165+
}
166+
167+
return withProgress(
168+
async (progress) => {
169+
const newDatabase = await downloadGitHubDatabaseFromUrl(
170+
database.url,
171+
database.id,
172+
database.created_at,
173+
database.commit_oid ?? null,
174+
owner,
175+
repo,
176+
octokit,
177+
progress,
178+
databaseManager,
179+
storagePath,
180+
cliServer,
181+
databaseManager.currentDatabaseItem === update.databaseItem,
182+
update.databaseItem.hasSourceArchiveInExplorer(),
183+
);
184+
if (newDatabase === undefined) {
185+
return;
186+
}
187+
188+
await databaseManager.removeDatabaseItem(update.databaseItem);
189+
190+
await commandManager.execute("codeQLDatabases.focus");
191+
void window.showInformationMessage(
192+
`Updated ${getLanguageDisplayName(
193+
database.language,
194+
)} database from GitHub.`,
195+
);
196+
},
197+
{
198+
title: `Updating ${getLanguageDisplayName(
199+
database.language,
200+
)} database from GitHub`,
201+
},
202+
);
203+
}),
204+
);
205+
}

0 commit comments

Comments
 (0)