Add copying of repository list for variant analyses

This adds the ability to copy the repository list for variant analyses
from the context menu in the query history.
This commit is contained in:
Koen Vlaswinkel
2022-11-11 15:14:28 +01:00
parent 4eb8c55045
commit 022d5c564f
4 changed files with 160 additions and 6 deletions

View File

@@ -934,6 +934,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,

View File

@@ -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;
}
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
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(): Promise<void> {

View File

@@ -1,7 +1,7 @@
import * as path from 'path';
import * as ghApiClient from './gh-api/gh-api-client';
import { CancellationToken, commands, EventEmitter, ExtensionContext, window } from 'vscode';
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, window } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { Credentials } from '../authentication';
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
@@ -24,6 +24,7 @@ import { processUpdatedVariantAnalysis, processVariantAnalysisRepositoryTask } f
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';
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
@@ -301,6 +302,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),

View File

@@ -1,6 +1,6 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { CancellationTokenSource, commands, extensions } from 'vscode';
import { CancellationTokenSource, commands, env, extensions } 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,
@@ -142,7 +145,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 {
@@ -585,4 +590,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,
],
});
});
});
});
});