Skip to content

Commit 022d5c5

Browse files
committed
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.
1 parent 4eb8c55 commit 022d5c5

File tree

4 files changed

+160
-6
lines changed

4 files changed

+160
-6
lines changed

extensions/ql-vscode/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,12 @@ async function activateWithInstalledDistribution(
934934
})
935935
);
936936

937+
ctx.subscriptions.push(
938+
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => {
939+
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId);
940+
})
941+
);
942+
937943
ctx.subscriptions.push(
938944
commandRunner('codeQL.monitorVariantAnalysis', async (
939945
variantAnalysis: VariantAnalysis,

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,11 +1256,15 @@ export class QueryHistoryManager extends DisposableObject {
12561256
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
12571257

12581258
// Remote queries only
1259-
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'remote') {
1259+
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
12601260
return;
12611261
}
12621262

1263-
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
1263+
if (finalSingleItem.t === 'remote') {
1264+
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
1265+
} else if (finalSingleItem.t === 'variant-analysis') {
1266+
await commands.executeCommand('codeQL.copyVariantAnalysisRepoList', finalSingleItem.variantAnalysis.id);
1267+
}
12641268
}
12651269

12661270
async handleExportResults(): Promise<void> {

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'path';
22

33
import * as ghApiClient from './gh-api/gh-api-client';
4-
import { CancellationToken, commands, EventEmitter, ExtensionContext, window } from 'vscode';
4+
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, window } from 'vscode';
55
import { DisposableObject } from '../pure/disposable-object';
66
import { Credentials } from '../authentication';
77
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
@@ -24,6 +24,7 @@ import { processUpdatedVariantAnalysis, processVariantAnalysisRepositoryTask } f
2424
import PQueue from 'p-queue';
2525
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers';
2626
import * as fs from 'fs-extra';
27+
import * as os from 'os';
2728
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
2829

2930
export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager<VariantAnalysisView> {
@@ -301,6 +302,27 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
301302
await cancelVariantAnalysis(credentials, variantAnalysis);
302303
}
303304

305+
public async copyRepoListToClipboard(variantAnalysisId: number) {
306+
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
307+
if (!variantAnalysis) {
308+
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
309+
}
310+
311+
const fullNames = variantAnalysis.scannedRepos?.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
312+
if (!fullNames || fullNames.length === 0) {
313+
return;
314+
}
315+
316+
const text = [
317+
'"new-repo-list": [',
318+
...fullNames.slice(0, -1).map(repo => ` "${repo}",`),
319+
` "${fullNames[fullNames.length - 1]}"`,
320+
']'
321+
];
322+
323+
await env.clipboard.writeText(text.join(os.EOL));
324+
}
325+
304326
private getRepoStatesStoragePath(variantAnalysisId: number): string {
305327
return path.join(
306328
this.getVariantAnalysisStorageLocation(variantAnalysisId),

extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as sinon from 'sinon';
22
import { expect } from 'chai';
3-
import { CancellationTokenSource, commands, extensions } from 'vscode';
3+
import { CancellationTokenSource, commands, env, extensions } from 'vscode';
44
import { CodeQLExtensionInterface } from '../../../extension';
55
import { logger } from '../../../logging';
66
import * as config from '../../../config';
@@ -16,7 +16,10 @@ import { storagePath } from '../global.helper';
1616
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
1717
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
1818
import * as VariantAnalysisModule from '../../../remote-queries/shared/variant-analysis';
19-
import { createMockScannedRepos } from '../../factories/remote-queries/shared/scanned-repositories';
19+
import {
20+
createMockScannedRepo,
21+
createMockScannedRepos
22+
} from '../../factories/remote-queries/shared/scanned-repositories';
2023
import {
2124
VariantAnalysis,
2225
VariantAnalysisScannedRepository,
@@ -142,7 +145,9 @@ describe('Variant Analysis Manager', async function() {
142145
});
143146

144147
describe('when credentials are invalid', async () => {
145-
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
148+
beforeEach(async () => {
149+
sandbox.stub(Credentials, 'initialize').resolves(undefined);
150+
});
146151

147152
it('should return early if credentials are wrong', async () => {
148153
try {
@@ -585,4 +590,121 @@ describe('Variant Analysis Manager', async function() {
585590
});
586591
});
587592
});
593+
594+
describe('copyRepoListToClipboard', async () => {
595+
let variantAnalysis: VariantAnalysis;
596+
let variantAnalysisStorageLocation: string;
597+
598+
let writeTextStub: sinon.SinonStub;
599+
600+
beforeEach(async () => {
601+
variantAnalysis = createMockVariantAnalysis({});
602+
603+
variantAnalysisStorageLocation = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
604+
await createTimestampFile(variantAnalysisStorageLocation);
605+
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
606+
607+
writeTextStub = sinon.stub();
608+
sinon.stub(env, 'clipboard').value({
609+
writeText: writeTextStub,
610+
});
611+
});
612+
613+
afterEach(() => {
614+
fs.rmSync(variantAnalysisStorageLocation, { recursive: true });
615+
});
616+
617+
describe('when the variant analysis does not have any repositories', () => {
618+
beforeEach(async () => {
619+
await variantAnalysisManager.rehydrateVariantAnalysis({
620+
...variantAnalysis,
621+
scannedRepos: [],
622+
});
623+
});
624+
625+
it('should not copy any text', async () => {
626+
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
627+
628+
expect(writeTextStub).not.to.have.been.called;
629+
});
630+
});
631+
632+
describe('when the variant analysis does not have any repositories with results', () => {
633+
beforeEach(async () => {
634+
await variantAnalysisManager.rehydrateVariantAnalysis({
635+
...variantAnalysis,
636+
scannedRepos: [
637+
{
638+
...createMockScannedRepo(),
639+
resultCount: 0,
640+
},
641+
{
642+
...createMockScannedRepo(),
643+
resultCount: undefined,
644+
}
645+
],
646+
});
647+
});
648+
649+
it('should not copy any text', async () => {
650+
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
651+
652+
expect(writeTextStub).not.to.have.been.called;
653+
});
654+
});
655+
656+
describe('when the variant analysis has repositories with results', () => {
657+
const scannedRepos = [
658+
{
659+
...createMockScannedRepo(),
660+
resultCount: 100,
661+
},
662+
{
663+
...createMockScannedRepo(),
664+
resultCount: 0,
665+
},
666+
{
667+
...createMockScannedRepo(),
668+
resultCount: 200,
669+
},
670+
{
671+
...createMockScannedRepo(),
672+
resultCount: undefined,
673+
},
674+
{
675+
...createMockScannedRepo(),
676+
resultCount: 5,
677+
},
678+
];
679+
680+
beforeEach(async () => {
681+
await variantAnalysisManager.rehydrateVariantAnalysis({
682+
...variantAnalysis,
683+
scannedRepos,
684+
});
685+
});
686+
687+
it('should copy text', async () => {
688+
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
689+
690+
expect(writeTextStub).to.have.been.calledOnce;
691+
});
692+
693+
it('should be valid JSON when put in object', async () => {
694+
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);
695+
696+
const text = writeTextStub.getCalls()[0].lastArg;
697+
698+
const parsed = JSON.parse('{' + text + '}');
699+
700+
expect(parsed).to.deep.eq({
701+
'new-repo-list': [
702+
scannedRepos[0].repository.fullName,
703+
scannedRepos[2].repository.fullName,
704+
scannedRepos[4].repository.fullName,
705+
],
706+
});
707+
});
708+
});
709+
});
588710
});

0 commit comments

Comments
 (0)