Loading package.json +13 −0 Original line number Diff line number Diff line Loading @@ -238,6 +238,11 @@ "command": "gl.clearSelectedProject", "title": "Clear selected project", "category": "GitLab" }, { "command": "gl.downloadArtifacts", "title": "Download artifacts", "category": "GitLab" } ], "menus": { Loading Loading @@ -286,6 +291,10 @@ "command": "gl.checkoutMrBranch", "when": "false" }, { "command": "gl.downloadArtifacts", "when": "false" }, { "command": "gl.openMrFile", "when": "false" Loading Loading @@ -424,6 +433,10 @@ { "command": "gl.clearSelectedProject", "when": "view =~ /issuesAndMrs/ && viewItem == selected-project" }, { "command": "gl.downloadArtifacts", "when": "view =~ /currentBranchInfo/ && viewItem == with-artifacts" } ], "comments/comment/title": [ Loading src/command_names.ts +1 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ export const USER_COMMANDS = { SELECT_PROJECT: 'gl.selectProject', ASSIGN_PROJECT: 'gl.assignProject', CLEAR_SELECTED_PROJECT: 'gl.clearSelectedProject', DOWNLOAD_ARTIFACTS: 'gl.downloadArtifacts', AUTHENTICATE: 'gl.authenticate', }; Loading src/commands/download_artifact.test.ts 0 → 100644 +75 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { VS_COMMANDS } from '../command_names'; import { asMock } from '../test_utils/as_mock'; import { job, artifact } from '../test_utils/entities'; import { JobItemModel } from '../tree_view/items/job_item_model'; import { StageItemModel } from '../tree_view/items/stage_item_model'; import { downloadArtifacts } from './download_artifact'; describe('downloadArtifacts', () => { const traceJob: RestJob = { ...job, artifacts: [{ ...artifact, file_type: 'trace' }] }; const archiveJob: RestJob = { ...job, name: 'test 3', artifacts: [{ ...artifact, file_type: 'archive' }], }; const junitJob: RestJob = { ...job, name: 'test 2', artifacts: [artifact] }; const multipleJob: RestJob = { ...job, name: 'test 1', artifacts: [{ ...artifact, file_type: 'cobertura' }, artifact], }; afterEach(() => { jest.resetAllMocks(); }); it('rejects jobs without artifacts', async () => { await downloadArtifacts(new JobItemModel(traceJob)); expect(vscode.window.showQuickPick).not.toBeCalled(); expect(vscode.commands.executeCommand).not.toBeCalled(); expect(vscode.window.showWarningMessage).toBeCalled(); }); it('displays the artifacts in a picker', async () => { await downloadArtifacts(new JobItemModel(multipleJob)); expect(vscode.window.showQuickPick).toBeCalled(); const items = asMock(vscode.window.showQuickPick).mock.lastCall[0]; expect(items).toHaveLength(2); expect(items[0].label).toBe('$(file) cobertura'); expect(items[1].label).toBe('$(file) junit'); }); it('displays the job name when multiple jobs are given', async () => { await downloadArtifacts(new StageItemModel('test', [archiveJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); const items = asMock(vscode.window.showQuickPick).mock.lastCall[0]; expect(items).toHaveLength(3); expect(items[0].label).toBe('$(file-zip) test 3:archive'); expect(items[1].label).toBe('$(file) test 1:cobertura'); expect(items[2].label).toBe('$(file) test 1:junit'); }); it('allows cancelling the picker', async () => { asMock(vscode.window.showQuickPick).mockResolvedValue(undefined); await downloadArtifacts(new StageItemModel('test', [junitJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); expect(vscode.commands.executeCommand).not.toBeCalled(); }); it('downloads the selected artifact', async () => { asMock(vscode.window.showQuickPick).mockImplementation(options => options[1]); await downloadArtifacts(new StageItemModel('test', [junitJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); const uri = `${multipleJob.web_url}/artifacts/download?file_type=cobertura`; expect(vscode.commands.executeCommand).toBeCalledWith(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); }); it('downloads immediately when only an archive is available', async () => { await downloadArtifacts(new JobItemModel(archiveJob)); expect(vscode.window.showQuickPick).not.toBeCalled(); const uri = `${archiveJob.web_url}/artifacts/download?file_type=archive`; expect(vscode.commands.executeCommand).toBeCalledWith(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); }); }); src/commands/download_artifact.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { VS_COMMANDS } from '../command_names'; import { isArtifactDownloadable, JobProvider } from '../tree_view/items/job_provider'; const first = <T>(a: T[]): T | undefined => a[0]; interface ArtifactPickItem extends vscode.QuickPickItem { job: RestJob; artifact: RestArtifact; } export async function downloadArtifacts(item: JobProvider): Promise<void> { let result: ArtifactPickItem | undefined; const jobs = item.getJobs(); const singleJob = jobs.length === 1 ? first(jobs) : null; const artifacts = jobs.flatMap( j => j.artifacts?.filter(isArtifactDownloadable).map(a => ({ artifact: a, job: j })) ?? [], ); if (!artifacts.length) { await vscode.window.showWarningMessage('This job does not have downloadable artifacts.'); return; } const items: ArtifactPickItem[] = artifacts.map(a => ({ label: `$(${a.artifact.file_type === 'archive' ? 'file-zip' : 'file'}) ${ singleJob ? '' : `${a.job.name}:` }${a.artifact.file_type}`, description: a.artifact.filename, job: a.job, artifact: a.artifact, })); if (singleJob && items.length === 1 && items[0].artifact.file_type === 'archive') { result = first(items); } else { result = await vscode.window.showQuickPick(items, { title: 'Download artifacts', }); } if (!result) return; const uri = `${result.job.web_url}/artifacts/download?file_type=${result.artifact.file_type}`; await vscode.commands.executeCommand(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); } src/extension.ts +2 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import { changeTypeDecorationProvider } from './review/change_type_decoration_pr import { checkoutMrBranch } from './commands/checkout_mr_branch'; import { cloneWiki } from './commands/clone_wiki'; import { createSnippetPatch } from './commands/create_snippet_patch'; import { downloadArtifacts } from './commands/download_artifact'; import { applySnippetPatch } from './commands/apply_snippet_patch'; import { openMrFile } from './commands/open_mr_file'; import { GitLabRemoteFileSystem } from './remotefs/gitlab_remote_file_system'; Loading Loading @@ -135,6 +136,7 @@ const registerCommands = ( [USER_COMMANDS.SELECT_PROJECT]: selectProjectCommand, [USER_COMMANDS.ASSIGN_PROJECT]: assignProject, [USER_COMMANDS.CLEAR_SELECTED_PROJECT]: clearSelectedProjects, [USER_COMMANDS.DOWNLOAD_ARTIFACTS]: downloadArtifacts, [PROGRAMMATIC_COMMANDS.NO_IMAGE_REVIEW]: () => vscode.window.showInformationMessage("GitLab MR review doesn't support images yet."), }; Loading Loading
package.json +13 −0 Original line number Diff line number Diff line Loading @@ -238,6 +238,11 @@ "command": "gl.clearSelectedProject", "title": "Clear selected project", "category": "GitLab" }, { "command": "gl.downloadArtifacts", "title": "Download artifacts", "category": "GitLab" } ], "menus": { Loading Loading @@ -286,6 +291,10 @@ "command": "gl.checkoutMrBranch", "when": "false" }, { "command": "gl.downloadArtifacts", "when": "false" }, { "command": "gl.openMrFile", "when": "false" Loading Loading @@ -424,6 +433,10 @@ { "command": "gl.clearSelectedProject", "when": "view =~ /issuesAndMrs/ && viewItem == selected-project" }, { "command": "gl.downloadArtifacts", "when": "view =~ /currentBranchInfo/ && viewItem == with-artifacts" } ], "comments/comment/title": [ Loading
src/command_names.ts +1 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,7 @@ export const USER_COMMANDS = { SELECT_PROJECT: 'gl.selectProject', ASSIGN_PROJECT: 'gl.assignProject', CLEAR_SELECTED_PROJECT: 'gl.clearSelectedProject', DOWNLOAD_ARTIFACTS: 'gl.downloadArtifacts', AUTHENTICATE: 'gl.authenticate', }; Loading
src/commands/download_artifact.test.ts 0 → 100644 +75 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { VS_COMMANDS } from '../command_names'; import { asMock } from '../test_utils/as_mock'; import { job, artifact } from '../test_utils/entities'; import { JobItemModel } from '../tree_view/items/job_item_model'; import { StageItemModel } from '../tree_view/items/stage_item_model'; import { downloadArtifacts } from './download_artifact'; describe('downloadArtifacts', () => { const traceJob: RestJob = { ...job, artifacts: [{ ...artifact, file_type: 'trace' }] }; const archiveJob: RestJob = { ...job, name: 'test 3', artifacts: [{ ...artifact, file_type: 'archive' }], }; const junitJob: RestJob = { ...job, name: 'test 2', artifacts: [artifact] }; const multipleJob: RestJob = { ...job, name: 'test 1', artifacts: [{ ...artifact, file_type: 'cobertura' }, artifact], }; afterEach(() => { jest.resetAllMocks(); }); it('rejects jobs without artifacts', async () => { await downloadArtifacts(new JobItemModel(traceJob)); expect(vscode.window.showQuickPick).not.toBeCalled(); expect(vscode.commands.executeCommand).not.toBeCalled(); expect(vscode.window.showWarningMessage).toBeCalled(); }); it('displays the artifacts in a picker', async () => { await downloadArtifacts(new JobItemModel(multipleJob)); expect(vscode.window.showQuickPick).toBeCalled(); const items = asMock(vscode.window.showQuickPick).mock.lastCall[0]; expect(items).toHaveLength(2); expect(items[0].label).toBe('$(file) cobertura'); expect(items[1].label).toBe('$(file) junit'); }); it('displays the job name when multiple jobs are given', async () => { await downloadArtifacts(new StageItemModel('test', [archiveJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); const items = asMock(vscode.window.showQuickPick).mock.lastCall[0]; expect(items).toHaveLength(3); expect(items[0].label).toBe('$(file-zip) test 3:archive'); expect(items[1].label).toBe('$(file) test 1:cobertura'); expect(items[2].label).toBe('$(file) test 1:junit'); }); it('allows cancelling the picker', async () => { asMock(vscode.window.showQuickPick).mockResolvedValue(undefined); await downloadArtifacts(new StageItemModel('test', [junitJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); expect(vscode.commands.executeCommand).not.toBeCalled(); }); it('downloads the selected artifact', async () => { asMock(vscode.window.showQuickPick).mockImplementation(options => options[1]); await downloadArtifacts(new StageItemModel('test', [junitJob, multipleJob])); expect(vscode.window.showQuickPick).toBeCalled(); const uri = `${multipleJob.web_url}/artifacts/download?file_type=cobertura`; expect(vscode.commands.executeCommand).toBeCalledWith(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); }); it('downloads immediately when only an archive is available', async () => { await downloadArtifacts(new JobItemModel(archiveJob)); expect(vscode.window.showQuickPick).not.toBeCalled(); const uri = `${archiveJob.web_url}/artifacts/download?file_type=archive`; expect(vscode.commands.executeCommand).toBeCalledWith(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); }); });
src/commands/download_artifact.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { VS_COMMANDS } from '../command_names'; import { isArtifactDownloadable, JobProvider } from '../tree_view/items/job_provider'; const first = <T>(a: T[]): T | undefined => a[0]; interface ArtifactPickItem extends vscode.QuickPickItem { job: RestJob; artifact: RestArtifact; } export async function downloadArtifacts(item: JobProvider): Promise<void> { let result: ArtifactPickItem | undefined; const jobs = item.getJobs(); const singleJob = jobs.length === 1 ? first(jobs) : null; const artifacts = jobs.flatMap( j => j.artifacts?.filter(isArtifactDownloadable).map(a => ({ artifact: a, job: j })) ?? [], ); if (!artifacts.length) { await vscode.window.showWarningMessage('This job does not have downloadable artifacts.'); return; } const items: ArtifactPickItem[] = artifacts.map(a => ({ label: `$(${a.artifact.file_type === 'archive' ? 'file-zip' : 'file'}) ${ singleJob ? '' : `${a.job.name}:` }${a.artifact.file_type}`, description: a.artifact.filename, job: a.job, artifact: a.artifact, })); if (singleJob && items.length === 1 && items[0].artifact.file_type === 'archive') { result = first(items); } else { result = await vscode.window.showQuickPick(items, { title: 'Download artifacts', }); } if (!result) return; const uri = `${result.job.web_url}/artifacts/download?file_type=${result.artifact.file_type}`; await vscode.commands.executeCommand(VS_COMMANDS.OPEN, vscode.Uri.parse(uri)); }
src/extension.ts +2 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import { changeTypeDecorationProvider } from './review/change_type_decoration_pr import { checkoutMrBranch } from './commands/checkout_mr_branch'; import { cloneWiki } from './commands/clone_wiki'; import { createSnippetPatch } from './commands/create_snippet_patch'; import { downloadArtifacts } from './commands/download_artifact'; import { applySnippetPatch } from './commands/apply_snippet_patch'; import { openMrFile } from './commands/open_mr_file'; import { GitLabRemoteFileSystem } from './remotefs/gitlab_remote_file_system'; Loading Loading @@ -135,6 +136,7 @@ const registerCommands = ( [USER_COMMANDS.SELECT_PROJECT]: selectProjectCommand, [USER_COMMANDS.ASSIGN_PROJECT]: assignProject, [USER_COMMANDS.CLEAR_SELECTED_PROJECT]: clearSelectedProjects, [USER_COMMANDS.DOWNLOAD_ARTIFACTS]: downloadArtifacts, [PROGRAMMATIC_COMMANDS.NO_IMAGE_REVIEW]: () => vscode.window.showInformationMessage("GitLab MR review doesn't support images yet."), }; Loading