Skip to content

Commit c829c30

Browse files
committed
MRVA: Add command to export markdown results to gist
1 parent 7947afb commit c829c30

File tree

7 files changed

+106
-5
lines changed

7 files changed

+106
-5
lines changed

extensions/ql-vscode/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@
301301
"command": "codeQL.runVariantAnalysis",
302302
"title": "CodeQL: Run Variant Analysis"
303303
},
304+
{
305+
"command": "codeQL.exportVariantAnalysisResults",
306+
"title": "CodeQL: Export Variant Analysis Results"
307+
},
304308
{
305309
"command": "codeQL.runQueries",
306310
"title": "CodeQL: Run Queries in Selected Files"
@@ -837,6 +841,10 @@
837841
"command": "codeQL.runVariantAnalysis",
838842
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
839843
},
844+
{
845+
"command": "codeQL.exportVariantAnalysisResults",
846+
"when": "config.codeQL.canary"
847+
},
840848
{
841849
"command": "codeQL.runQueries",
842850
"when": "false"

extensions/ql-vscode/src/authentication.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import * as Octokit from '@octokit/rest';
33

44
const GITHUB_AUTH_PROVIDER_ID = 'github';
55

6-
// 'repo' scope should be enough for triggering workflows. For a comprehensive list, see:
6+
// We need 'repo' scope for triggering workflows and 'gist' scope for exporting results to Gist.
7+
// For a comprehensive list of scopes, see:
78
// https://docs.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps
8-
const SCOPES = ['repo'];
9+
const SCOPES = ['repo', 'gist'];
910

1011
/**
1112
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).

extensions/ql-vscode/src/extension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,11 @@ async function activateWithInstalledDistribution(
895895
await rqm.autoDownloadRemoteQueryResults(queryResult, token);
896896
}));
897897

898+
ctx.subscriptions.push(
899+
commandRunner('codeQL.exportVariantAnalysisResults', async () => {
900+
await rqm.exportVariantAnalysisResults();
901+
}));
902+
898903
ctx.subscriptions.push(
899904
commandRunner(
900905
'codeQL.openReferencedFile',

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,10 @@ export class QueryHistoryManager extends DisposableObject {
590590
}
591591
}
592592

593+
getCurrentQueryHistoryItem(): QueryHistoryInfo | undefined {
594+
return this.treeDataProvider.getCurrent();
595+
}
596+
593597
async handleRemoveHistoryItem(
594598
singleItem: QueryHistoryInfo,
595599
multiSelect: QueryHistoryInfo[] = []

extensions/ql-vscode/src/remote-queries/gh-actions-api-client.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,25 @@ function getWorkflowError(conclusion: string | null): string {
311311

312312
return `Unexpected variant analysis execution conclusion: ${conclusion}`;
313313
}
314+
315+
/**
316+
* Creates a gist with the given description and files.
317+
* Returns the URL of the created gist.
318+
*/
319+
export async function createGist(
320+
credentials: Credentials,
321+
description: string,
322+
files: { [key: string]: { content: string } }
323+
): Promise<string | undefined> {
324+
const octokit = await credentials.getOctokit();
325+
console.log(description, files);
326+
const response = await octokit.request('POST /gists', {
327+
description,
328+
files,
329+
public: false,
330+
});
331+
if (response.status >= 300) {
332+
throw new Error(`Error exporting variant analysis results: ${response.status} ${response?.data || ''}`);
333+
}
334+
return response.data.html_url;
335+
}

extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,9 @@ export class RemoteQueriesInterfaceManager {
305305
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes)
306306
}));
307307
}
308+
309+
/** Gets the current query ID */
310+
public getCurrentQueryId(): string | undefined {
311+
return this.currentQueryId;
312+
}
308313
}

extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import * as fs from 'fs-extra';
55

66
import { Credentials } from '../authentication';
77
import { CodeQLCliServer } from '../cli';
8-
import { ProgressCallback } from '../commandRunner';
8+
import { ProgressCallback, UserCancellationException } from '../commandRunner';
99
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage, showInformationMessageWithAction } from '../helpers';
1010
import { Logger } from '../logging';
1111
import { runRemoteQuery } from './run-remote-query';
1212
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
1313
import { RemoteQuery } from './remote-query';
1414
import { RemoteQueriesMonitor } from './remote-queries-monitor';
15-
import { getRemoteQueryIndex } from './gh-actions-api-client';
15+
import { createGist, getRemoteQueryIndex } from './gh-actions-api-client';
1616
import { RemoteQueryResultIndex } from './remote-query-result-index';
1717
import { RemoteQueryResult } from './remote-query-result';
1818
import { DownloadLink } from './download-link';
@@ -23,6 +23,7 @@ import { QueryHistoryManager } from '../query-history';
2323
import { QueryStatus } from '../query-status';
2424
import { DisposableObject } from '../pure/disposable-object';
2525
import { QueryHistoryInfo } from '../query-results';
26+
import { generateMarkdown } from './remote-queries-markdown-generation';
2627

2728
const autoDownloadMaxSize = 300 * 1024;
2829
const autoDownloadMaxCount = 100;
@@ -38,7 +39,7 @@ export class RemoteQueriesManager extends DisposableObject {
3839
private readonly cliServer: CodeQLCliServer,
3940
private readonly qhm: QueryHistoryManager,
4041
private readonly storagePath: string,
41-
logger: Logger,
42+
private readonly logger: Logger,
4243
) {
4344
super();
4445
this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger);
@@ -301,4 +302,59 @@ export class RemoteQueriesManager extends DisposableObject {
301302
queryItem.status = QueryStatus.Failed;
302303
}
303304
}
305+
306+
public async exportVariantAnalysisResults(): Promise<void> {
307+
const queryId = this.interfaceManager.getCurrentQueryId();
308+
const queryHistoryItem = this.qhm.getCurrentQueryHistoryItem();
309+
310+
if (!queryId || !queryHistoryItem || !queryHistoryItem.completed || queryHistoryItem.t !== 'remote') {
311+
throw new Error('No variant analysis results currently open. To open results, click an item in the query history view.');
312+
}
313+
314+
void this.logger.log(`Exporting variant analysis results for query: ${queryId}`);
315+
const query = queryHistoryItem.remoteQuery;
316+
const analysesResults = this.analysesResultsManager.getAnalysesResults(queryId);
317+
318+
const credentials = await Credentials.initialize(this.ctx);
319+
320+
const gistOption = {
321+
label: '$(ports-open-browser-icon) Create Gist (GitHub)',
322+
};
323+
const localMarkdownOption = {
324+
label: '$(markdown) Save as markdown',
325+
};
326+
327+
// User selects export format in quick pick
328+
const exportFormat = await window.showQuickPick(
329+
[gistOption, localMarkdownOption],
330+
{
331+
placeHolder: 'Select export format',
332+
canPickMany: false,
333+
ignoreFocusOut: true,
334+
}
335+
);
336+
337+
if (!exportFormat || !exportFormat.label) {
338+
throw new UserCancellationException('No export format selected', true);
339+
}
340+
341+
if (exportFormat === gistOption) {
342+
const description = 'CodeQL Variant Analysis Results';
343+
344+
const markdownFiles = generateMarkdown(query, analysesResults, 'gist');
345+
346+
// Convert markdownFiles to the appropriate format for uploading to gist
347+
const gistFiles = markdownFiles.reduce((acc, cur) => {
348+
acc[`${cur.fileName}.md`] = { content: cur.content.join('\n') };
349+
return acc;
350+
}, {} as { [key: string]: { content: string } });
351+
352+
const gistUrl = await createGist(credentials, description, gistFiles);
353+
void showAndLogInformationMessage(`Variant analysis results exported to [gist](${gistUrl}).`);
354+
} else if (exportFormat === localMarkdownOption) {
355+
// TODO: Write function that creates local markdown files
356+
// const markdownFiles = generateMarkdown(query, analysesResults, 'local');
357+
void showAndLogInformationMessage('Local markdown export not yet available');
358+
}
359+
}
304360
}

0 commit comments

Comments
 (0)