Merge remote-tracking branch 'origin/main' into koesie10/export-results
This commit is contained in:
5
extensions/ql-vscode/src/common/app.ts
Normal file
5
extensions/ql-vscode/src/common/app.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { AppEventEmitter } from './events';
|
||||
|
||||
export interface App {
|
||||
createEventEmitter<T>(): AppEventEmitter<T>;
|
||||
}
|
||||
10
extensions/ql-vscode/src/common/events.ts
Normal file
10
extensions/ql-vscode/src/common/events.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Disposable } from '../pure/disposable-object';
|
||||
|
||||
export interface AppEvent<T> {
|
||||
(listener: (event: T) => void): Disposable;
|
||||
}
|
||||
|
||||
export interface AppEventEmitter<T> {
|
||||
event: AppEvent<T>;
|
||||
fire(data: T): void;
|
||||
}
|
||||
7
extensions/ql-vscode/src/common/vscode/events.ts
Normal file
7
extensions/ql-vscode/src/common/vscode/events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { AppEventEmitter } from '../events';
|
||||
|
||||
export class VSCodeAppEventEmitter<T>
|
||||
extends vscode.EventEmitter<T>
|
||||
implements AppEventEmitter<T> {
|
||||
}
|
||||
9
extensions/ql-vscode/src/common/vscode/vscode-app.ts
Normal file
9
extensions/ql-vscode/src/common/vscode/vscode-app.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { App } from '../app';
|
||||
import { AppEventEmitter } from '../events';
|
||||
import { VSCodeAppEventEmitter } from './events';
|
||||
|
||||
export class ExtensionApp implements App {
|
||||
public createEventEmitter<T>(): AppEventEmitter<T> {
|
||||
return new VSCodeAppEventEmitter<T>();
|
||||
}
|
||||
}
|
||||
@@ -948,6 +948,12 @@ async function activateWithInstalledDistribution(
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId);
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.monitorVariantAnalysis', async (
|
||||
variantAnalysis: VariantAnalysis,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
// Avoid explicitly referencing Disposable type in vscode.
|
||||
// This file cannot have dependencies on the vscode API.
|
||||
interface Disposable {
|
||||
export interface Disposable {
|
||||
dispose(): any;
|
||||
}
|
||||
|
||||
|
||||
@@ -472,6 +472,10 @@ export interface OpenQueryTextMessage {
|
||||
t: 'openQueryText';
|
||||
}
|
||||
|
||||
export interface CopyRepositoryListMessage {
|
||||
t: 'copyRepositoryList';
|
||||
}
|
||||
|
||||
export interface OpenLogsMessage {
|
||||
t: 'openLogs';
|
||||
}
|
||||
@@ -490,5 +494,6 @@ export type FromVariantAnalysisMessage =
|
||||
| RequestRepositoryResultsMessage
|
||||
| OpenQueryFileMessage
|
||||
| OpenQueryTextMessage
|
||||
| CopyRepositoryListMessage
|
||||
| OpenLogsMessage
|
||||
| CancelVariantAnalysisMessage;
|
||||
|
||||
@@ -1256,11 +1256,15 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
// Remote queries only
|
||||
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'remote') {
|
||||
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalSingleItem.t === 'remote') {
|
||||
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
|
||||
} else if (finalSingleItem.t === 'variant-analysis') {
|
||||
await commands.executeCommand('codeQL.copyVariantAnalysisRepoList', finalSingleItem.variantAnalysis.id);
|
||||
}
|
||||
}
|
||||
|
||||
async handleExportResults(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
import { CancellationToken, commands, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
|
||||
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { Credentials } from '../authentication';
|
||||
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import PQueue from 'p-queue';
|
||||
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as os from 'os';
|
||||
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
|
||||
import { ProgressCallback, UserCancellationException } from '../commandRunner';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
@@ -388,6 +389,27 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
await cancelVariantAnalysis(credentials, variantAnalysis);
|
||||
}
|
||||
|
||||
public async copyRepoListToClipboard(variantAnalysisId: number) {
|
||||
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
||||
if (!variantAnalysis) {
|
||||
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
||||
}
|
||||
|
||||
const fullNames = variantAnalysis.scannedRepos?.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
|
||||
if (!fullNames || fullNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = [
|
||||
'"new-repo-list": [',
|
||||
...fullNames.slice(0, -1).map(repo => ` "${repo}",`),
|
||||
` "${fullNames[fullNames.length - 1]}"`,
|
||||
']'
|
||||
];
|
||||
|
||||
await env.clipboard.writeText(text.join(os.EOL));
|
||||
}
|
||||
|
||||
private getRepoStatesStoragePath(variantAnalysisId: number): string {
|
||||
return path.join(
|
||||
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
||||
|
||||
@@ -103,6 +103,9 @@ export class VariantAnalysisView extends AbstractWebview<ToVariantAnalysisMessag
|
||||
case 'openQueryText':
|
||||
await this.openQueryText();
|
||||
break;
|
||||
case 'copyRepositoryList':
|
||||
void commands.executeCommand('codeQL.copyVariantAnalysisRepoList', this.variantAnalysisId);
|
||||
break;
|
||||
case 'openLogs':
|
||||
await this.openLogs();
|
||||
break;
|
||||
|
||||
@@ -36,6 +36,12 @@ const stopQuery = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const copyRepositoryList = () => {
|
||||
vscode.postMessage({
|
||||
t: 'copyRepositoryList',
|
||||
});
|
||||
};
|
||||
|
||||
const openLogs = () => {
|
||||
vscode.postMessage({
|
||||
t: 'openLogs',
|
||||
@@ -95,7 +101,7 @@ export function VariantAnalysis({
|
||||
onOpenQueryFileClick={openQueryFile}
|
||||
onViewQueryTextClick={openQueryText}
|
||||
onStopQueryClick={stopQuery}
|
||||
onCopyRepositoryListClick={() => console.log('Copy repository list')}
|
||||
onCopyRepositoryListClick={copyRepositoryList}
|
||||
onExportResultsClick={() => console.log('Export results')}
|
||||
onViewLogsClick={openLogs}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { assert, expect } from 'chai';
|
||||
import { CancellationTokenSource, commands, extensions, QuickPickItem, Uri, window } from 'vscode';
|
||||
import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri, window } from 'vscode';
|
||||
import { CodeQLExtensionInterface } from '../../../extension';
|
||||
import { logger } from '../../../logging';
|
||||
import * as config from '../../../config';
|
||||
@@ -16,7 +16,10 @@ import { storagePath } from '../global.helper';
|
||||
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
|
||||
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
|
||||
import * as VariantAnalysisModule from '../../../remote-queries/shared/variant-analysis';
|
||||
import { createMockScannedRepos } from '../../factories/remote-queries/shared/scanned-repositories';
|
||||
import {
|
||||
createMockScannedRepo,
|
||||
createMockScannedRepos
|
||||
} from '../../factories/remote-queries/shared/scanned-repositories';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
@@ -252,7 +255,9 @@ describe('Variant Analysis Manager', async function() {
|
||||
});
|
||||
|
||||
describe('when credentials are invalid', async () => {
|
||||
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
|
||||
beforeEach(async () => {
|
||||
sandbox.stub(Credentials, 'initialize').resolves(undefined);
|
||||
});
|
||||
|
||||
it('should return early if credentials are wrong', async () => {
|
||||
try {
|
||||
@@ -695,4 +700,121 @@ describe('Variant Analysis Manager', async function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyRepoListToClipboard', async () => {
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
let variantAnalysisStorageLocation: string;
|
||||
|
||||
let writeTextStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
variantAnalysis = createMockVariantAnalysis({});
|
||||
|
||||
variantAnalysisStorageLocation = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
|
||||
await createTimestampFile(variantAnalysisStorageLocation);
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
|
||||
|
||||
writeTextStub = sinon.stub();
|
||||
sinon.stub(env, 'clipboard').value({
|
||||
writeText: writeTextStub,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(variantAnalysisStorageLocation, { recursive: true });
|
||||
});
|
||||
|
||||
describe('when the variant analysis does not have any repositories', () => {
|
||||
beforeEach(async () => {
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis({
|
||||
...variantAnalysis,
|
||||
scannedRepos: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not copy any text', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
|
||||
|
||||
expect(writeTextStub).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the variant analysis does not have any repositories with results', () => {
|
||||
beforeEach(async () => {
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis({
|
||||
...variantAnalysis,
|
||||
scannedRepos: [
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: 0,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: undefined,
|
||||
}
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not copy any text', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
|
||||
|
||||
expect(writeTextStub).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the variant analysis has repositories with results', () => {
|
||||
const scannedRepos = [
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: 100,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: 0,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: 200,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: undefined,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
resultCount: 5,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis({
|
||||
...variantAnalysis,
|
||||
scannedRepos,
|
||||
});
|
||||
});
|
||||
|
||||
it('should copy text', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
|
||||
|
||||
expect(writeTextStub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should be valid JSON when put in object', async () => {
|
||||
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
|
||||
|
||||
const text = writeTextStub.getCalls()[0].lastArg;
|
||||
|
||||
const parsed = JSON.parse('{' + text + '}');
|
||||
|
||||
expect(parsed).to.deep.eq({
|
||||
'new-repo-list': [
|
||||
scannedRepos[0].repository.fullName,
|
||||
scannedRepos[2].repository.fullName,
|
||||
scannedRepos[4].repository.fullName,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user