Skip to content

Commit c88b320

Browse files
authored
Merge pull request #1750 from github/koesie10/export-results
Add exporting of results from the context menu
2 parents 542a78e + 5ed4981 commit c88b320

File tree

10 files changed

+415
-73
lines changed

10 files changed

+415
-73
lines changed

extensions/ql-vscode/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
"title": "CodeQL: Run Variant Analysis"
326326
},
327327
{
328-
"command": "codeQL.exportVariantAnalysisResults",
328+
"command": "codeQL.exportSelectedVariantAnalysisResults",
329329
"title": "CodeQL: Export Variant Analysis Results"
330330
},
331331
{
@@ -954,7 +954,7 @@
954954
"when": "config.codeQL.canary && config.codeQL.variantAnalysis.liveResults"
955955
},
956956
{
957-
"command": "codeQL.exportVariantAnalysisResults",
957+
"command": "codeQL.exportSelectedVariantAnalysisResults",
958958
"when": "config.codeQL.canary"
959959
},
960960
{

extensions/ql-vscode/src/extension.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ import { RemoteQueryResult } from './remote-queries/remote-query-result';
9898
import { URLSearchParams } from 'url';
9999
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
100100
import { HistoryItemLabelProvider } from './history-item-label-provider';
101-
import { exportRemoteQueryResults } from './remote-queries/export-results';
101+
import {
102+
exportRemoteQueryResults,
103+
exportSelectedRemoteQueryResults,
104+
exportVariantAnalysisResults
105+
} from './remote-queries/export-results';
102106
import { RemoteQuery } from './remote-queries/remote-query';
103107
import { EvalLogViewer } from './eval-log-viewer';
104108
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
@@ -992,11 +996,23 @@ async function activateWithInstalledDistribution(
992996
}));
993997

994998
ctx.subscriptions.push(
995-
commandRunner('codeQL.exportVariantAnalysisResults', async (queryId?: string) => {
999+
commandRunner('codeQL.exportSelectedVariantAnalysisResults', async () => {
1000+
await exportSelectedRemoteQueryResults(qhm);
1001+
})
1002+
);
1003+
1004+
ctx.subscriptions.push(
1005+
commandRunner('codeQL.exportRemoteQueryResults', async (queryId: string) => {
9961006
await exportRemoteQueryResults(qhm, rqm, ctx, queryId);
9971007
})
9981008
);
9991009

1010+
ctx.subscriptions.push(
1011+
commandRunner('codeQL.exportVariantAnalysisResults', async (variantAnalysisId: number) => {
1012+
await exportVariantAnalysisResults(ctx, variantAnalysisManager, variantAnalysisId);
1013+
})
1014+
);
1015+
10001016
ctx.subscriptions.push(
10011017
commandRunner('codeQL.loadVariantAnalysisRepoResults', async (variantAnalysisId: number, repositoryFullName: string) => {
10021018
await variantAnalysisManager.loadResults(variantAnalysisId, repositoryFullName);

extensions/ql-vscode/src/query-history.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,8 +1267,22 @@ export class QueryHistoryManager extends DisposableObject {
12671267
}
12681268
}
12691269

1270-
async handleExportResults(): Promise<void> {
1271-
await commands.executeCommand('codeQL.exportVariantAnalysisResults');
1270+
async handleExportResults(
1271+
singleItem: QueryHistoryInfo,
1272+
multiSelect: QueryHistoryInfo[],
1273+
): Promise<void> {
1274+
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
1275+
1276+
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
1277+
return;
1278+
}
1279+
1280+
// Remote queries and variant analysis only
1281+
if (finalSingleItem.t === 'remote') {
1282+
await commands.executeCommand('codeQL.exportRemoteQueryResults', finalSingleItem.queryId);
1283+
} else if (finalSingleItem.t === 'variant-analysis') {
1284+
await commands.executeCommand('codeQL.exportVariantAnalysisResults', finalSingleItem.variantAnalysis.id);
1285+
}
12721286
}
12731287

12741288
addQuery(item: QueryHistoryInfo) {

extensions/ql-vscode/src/remote-queries/export-results.ts

Lines changed: 175 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,62 @@
11
import * as path from 'path';
22
import * as fs from 'fs-extra';
33

4-
import { window, commands, Uri, ExtensionContext, QuickPickItem, workspace, ViewColumn } from 'vscode';
4+
import { window, commands, Uri, ExtensionContext, workspace, ViewColumn } from 'vscode';
55
import { Credentials } from '../authentication';
66
import { UserCancellationException } from '../commandRunner';
77
import { showInformationMessageWithAction } from '../helpers';
88
import { logger } from '../logging';
99
import { QueryHistoryManager } from '../query-history';
1010
import { createGist } from './gh-api/gh-api-client';
1111
import { RemoteQueriesManager } from './remote-queries-manager';
12-
import { generateMarkdown } from './remote-queries-markdown-generation';
12+
import {
13+
generateMarkdown,
14+
generateVariantAnalysisMarkdown,
15+
MarkdownFile,
16+
} from './remote-queries-markdown-generation';
1317
import { RemoteQuery } from './remote-query';
1418
import { AnalysisResults, sumAnalysesResults } from './shared/analysis-result';
15-
import { RemoteQueryHistoryItem } from './remote-query-history-item';
1619
import { pluralize } from '../pure/word';
20+
import { VariantAnalysisManager } from './variant-analysis-manager';
21+
import { assertNever } from '../pure/helpers-pure';
22+
import {
23+
VariantAnalysis,
24+
VariantAnalysisScannedRepository,
25+
VariantAnalysisScannedRepositoryResult
26+
} from './shared/variant-analysis';
1727

1828
/**
19-
* Exports the results of the given or currently-selected remote query.
29+
* Exports the results of the currently-selected remote query or variant analysis.
30+
*/
31+
export async function exportSelectedRemoteQueryResults(queryHistoryManager: QueryHistoryManager): Promise<void> {
32+
const queryHistoryItem = queryHistoryManager.getCurrentQueryHistoryItem();
33+
if (!queryHistoryItem || queryHistoryItem.t === 'local') {
34+
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
35+
}
36+
37+
if (queryHistoryItem.t === 'remote') {
38+
return commands.executeCommand('codeQL.exportRemoteQueryResults', queryHistoryItem.queryId);
39+
} else if (queryHistoryItem.t === 'variant-analysis') {
40+
return commands.executeCommand('codeQL.exportVariantAnalysisResults', queryHistoryItem.variantAnalysis.id);
41+
} else {
42+
assertNever(queryHistoryItem);
43+
}
44+
}
45+
46+
/**
47+
* Exports the results of the given remote query.
2048
* The user is prompted to select the export format.
2149
*/
2250
export async function exportRemoteQueryResults(
2351
queryHistoryManager: QueryHistoryManager,
2452
remoteQueriesManager: RemoteQueriesManager,
2553
ctx: ExtensionContext,
26-
queryId?: string,
54+
queryId: string,
2755
): Promise<void> {
28-
let queryHistoryItem: RemoteQueryHistoryItem;
29-
if (queryId) {
30-
const query = queryHistoryManager.getRemoteQueryById(queryId);
31-
if (!query) {
32-
void logger.log(`Could not find query with id ${queryId}`);
33-
throw new Error('There was an error when trying to retrieve variant analysis information');
34-
}
35-
queryHistoryItem = query;
36-
} else {
37-
const query = queryHistoryManager.getCurrentQueryHistoryItem();
38-
if (!query || query.t !== 'remote') {
39-
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
40-
}
41-
queryHistoryItem = query;
56+
const queryHistoryItem = queryHistoryManager.getRemoteQueryById(queryId);
57+
if (!queryHistoryItem) {
58+
void logger.log(`Could not find query with id ${queryId}`);
59+
throw new Error('There was an error when trying to retrieve variant analysis information');
4260
}
4361

4462
if (!queryHistoryItem.completed) {
@@ -49,32 +67,107 @@ export async function exportRemoteQueryResults(
4967
const query = queryHistoryItem.remoteQuery;
5068
const analysesResults = remoteQueriesManager.getAnalysesResults(queryHistoryItem.queryId);
5169

52-
const gistOption = {
53-
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
54-
};
55-
const localMarkdownOption = {
56-
label: '$(markdown) Save as markdown',
57-
};
58-
const exportFormat = await determineExportFormat(gistOption, localMarkdownOption);
70+
const exportFormat = await determineExportFormat();
71+
if (!exportFormat) {
72+
return;
73+
}
5974

60-
if (exportFormat === gistOption) {
61-
await exportResultsToGist(ctx, query, analysesResults);
62-
} else if (exportFormat === localMarkdownOption) {
63-
const queryDirectoryPath = await queryHistoryManager.getQueryHistoryItemDirectory(
64-
queryHistoryItem
65-
);
66-
await exportResultsToLocalMarkdown(queryDirectoryPath, query, analysesResults);
75+
const exportDirectory = await queryHistoryManager.getQueryHistoryItemDirectory(queryHistoryItem);
76+
77+
await exportRemoteQueryAnalysisResults(ctx, exportDirectory, query, analysesResults, exportFormat);
78+
}
79+
80+
export async function exportRemoteQueryAnalysisResults(
81+
ctx: ExtensionContext,
82+
exportDirectory: string,
83+
query: RemoteQuery,
84+
analysesResults: AnalysisResults[],
85+
exportFormat: 'gist' | 'local',
86+
) {
87+
const description = buildGistDescription(query, analysesResults);
88+
const markdownFiles = generateMarkdown(query, analysesResults, exportFormat);
89+
90+
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
91+
}
92+
93+
/**
94+
* Exports the results of the given or currently-selected remote query.
95+
* The user is prompted to select the export format.
96+
*/
97+
export async function exportVariantAnalysisResults(
98+
ctx: ExtensionContext,
99+
variantAnalysisManager: VariantAnalysisManager,
100+
variantAnalysisId: number,
101+
): Promise<void> {
102+
const variantAnalysis = await variantAnalysisManager.getVariantAnalysis(variantAnalysisId);
103+
if (!variantAnalysis) {
104+
void logger.log(`Could not find variant analysis with id ${variantAnalysisId}`);
105+
throw new Error('There was an error when trying to retrieve variant analysis information');
67106
}
107+
108+
void logger.log(`Exporting variant analysis results for variant analysis with id ${variantAnalysis.id}`);
109+
110+
const exportFormat = await determineExportFormat();
111+
if (!exportFormat) {
112+
return;
113+
}
114+
115+
async function* getAnalysesResults(): AsyncGenerator<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]> {
116+
if (!variantAnalysis?.scannedRepos) {
117+
return;
118+
}
119+
120+
for (const repo of variantAnalysis.scannedRepos) {
121+
if (repo.resultCount == 0) {
122+
yield [repo, {
123+
variantAnalysisId: variantAnalysis.id,
124+
repositoryId: repo.repository.id,
125+
}];
126+
continue;
127+
}
128+
129+
const result = await variantAnalysisManager.loadResults(variantAnalysis.id, repo.repository.fullName, {
130+
skipCacheStore: true,
131+
});
132+
133+
yield [repo, result];
134+
}
135+
}
136+
137+
const exportDirectory = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
138+
139+
await exportVariantAnalysisAnalysisResults(ctx, exportDirectory, variantAnalysis, getAnalysesResults(), exportFormat);
140+
}
141+
142+
export async function exportVariantAnalysisAnalysisResults(
143+
ctx: ExtensionContext,
144+
exportDirectory: string,
145+
variantAnalysis: VariantAnalysis,
146+
analysesResults: AsyncIterable<[VariantAnalysisScannedRepository, VariantAnalysisScannedRepositoryResult]>,
147+
exportFormat: 'gist' | 'local',
148+
) {
149+
const description = buildVariantAnalysisGistDescription(variantAnalysis);
150+
const markdownFiles = await generateVariantAnalysisMarkdown(variantAnalysis, analysesResults, 'gist');
151+
152+
await exportResults(ctx, exportDirectory, description, markdownFiles, exportFormat);
68153
}
69154

70155
/**
71156
* Determines the format in which to export the results, from the given export options.
72157
*/
73-
async function determineExportFormat(
74-
...options: { label: string }[]
75-
): Promise<QuickPickItem> {
158+
async function determineExportFormat(): Promise<'gist' | 'local' | undefined> {
159+
const gistOption = {
160+
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
161+
};
162+
const localMarkdownOption = {
163+
label: '$(markdown) Save as markdown',
164+
};
165+
76166
const exportFormat = await window.showQuickPick(
77-
options,
167+
[
168+
gistOption,
169+
localMarkdownOption,
170+
],
78171
{
79172
placeHolder: 'Select export format',
80173
canPickMany: false,
@@ -84,20 +177,38 @@ async function determineExportFormat(
84177
if (!exportFormat || !exportFormat.label) {
85178
throw new UserCancellationException('No export format selected', true);
86179
}
87-
return exportFormat;
180+
181+
if (exportFormat === gistOption) {
182+
return 'gist';
183+
}
184+
if (exportFormat === localMarkdownOption) {
185+
return 'local';
186+
}
187+
188+
return undefined;
88189
}
89190

90-
/**
91-
* Converts the results of a remote query to markdown and uploads the files as a secret gist.
92-
*/
93-
export async function exportResultsToGist(
191+
export async function exportResults(
94192
ctx: ExtensionContext,
95-
query: RemoteQuery,
96-
analysesResults: AnalysisResults[]
97-
): Promise<void> {
193+
exportDirectory: string,
194+
description: string,
195+
markdownFiles: MarkdownFile[],
196+
exportFormat: 'gist' | 'local',
197+
) {
198+
if (exportFormat === 'gist') {
199+
await exportToGist(ctx, description, markdownFiles);
200+
} else if (exportFormat === 'local') {
201+
await exportToLocalMarkdown(exportDirectory, markdownFiles);
202+
}
203+
}
204+
205+
export async function exportToGist(
206+
ctx: ExtensionContext,
207+
description: string,
208+
markdownFiles: MarkdownFile[]
209+
) {
98210
const credentials = await Credentials.initialize(ctx);
99-
const description = buildGistDescription(query, analysesResults);
100-
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
211+
101212
// Convert markdownFiles to the appropriate format for uploading to gist
102213
const gistFiles = markdownFiles.reduce((acc, cur) => {
103214
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
@@ -128,16 +239,25 @@ const buildGistDescription = (query: RemoteQuery, analysesResults: AnalysisResul
128239
};
129240

130241
/**
131-
* Converts the results of a remote query to markdown and saves the files locally
132-
* in the query directory (where query results and metadata are also saved).
242+
* Builds Gist description
243+
* Ex: Empty Block (Go) x results (y repositories)
133244
*/
134-
async function exportResultsToLocalMarkdown(
135-
queryDirectoryPath: string,
136-
query: RemoteQuery,
137-
analysesResults: AnalysisResults[]
245+
const buildVariantAnalysisGistDescription = (variantAnalysis: VariantAnalysis) => {
246+
const resultCount = variantAnalysis.scannedRepos?.reduce((acc, item) => acc + (item.resultCount ?? 0), 0) ?? 0;
247+
const resultLabel = pluralize(resultCount, 'result', 'results');
248+
249+
const repositoryLabel = variantAnalysis.scannedRepos?.length ? `(${pluralize(variantAnalysis.scannedRepos.length, 'repository', 'repositories')})` : '';
250+
return `${variantAnalysis.query.name} (${variantAnalysis.query.language}) ${resultLabel} ${repositoryLabel}`;
251+
};
252+
253+
/**
254+
* Saves the results of an exported query to local markdown files.
255+
*/
256+
async function exportToLocalMarkdown(
257+
exportDirectory: string,
258+
markdownFiles: MarkdownFile[],
138259
) {
139-
const markdownFiles = generateMarkdown(query, analysesResults, 'local');
140-
const exportedResultsPath = path.join(queryDirectoryPath, 'exported-results');
260+
const exportedResultsPath = path.join(exportDirectory, 'exported-results');
141261
await fs.ensureDir(exportedResultsPath);
142262
for (const markdownFile of markdownFiles) {
143263
const filePath = path.join(exportedResultsPath, `${markdownFile.fileName}.md`);

0 commit comments

Comments
 (0)