From 750bae4b2b8616bdf424a96f248ee51439351a1a Mon Sep 17 00:00:00 2001 From: Tomas Vik Date: Mon, 5 Jul 2021 12:00:01 +0200 Subject: [PATCH] feat: create snippet patch --- package.json | 8 +++ src/__mocks__/vscode.js | 3 +- src/command_names.ts | 1 + src/commands/create_snippet.ts | 31 +++++----- src/commands/create_snippet_patch.test.ts | 70 +++++++++++++++++++++++ src/commands/create_snippet_patch.ts | 63 ++++++++++++++++++++ src/extension.js | 2 + src/git/wrapped_repository.ts | 4 ++ src/status_bar.test.ts | 3 +- src/test_utils/as_mock.ts | 1 + 10 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 src/commands/create_snippet_patch.test.ts create mode 100644 src/commands/create_snippet_patch.ts create mode 100644 src/test_utils/as_mock.ts diff --git a/package.json b/package.json index 8363f25..7b8ebee 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,10 @@ "command": "gl.openProjectPage", "title": "GitLab: Open current project on GitLab" }, + { + "command": "gl.createSnippetPatch", + "title": "GitLab: Create snippet patch" + }, { "command": "gl.openCurrentPipeline", "title": "GitLab: Open current pipeline on GitLab" @@ -289,6 +293,10 @@ { "command": "gl.cloneWiki", "when": "!gitlab:noToken" + }, + { + "command": "gl.createSnippetPatch", + "when": "gitlab:validState" } ], "view/title": [ diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index 8b92893..c78a353 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -23,8 +23,9 @@ module.exports = { showWarningMessage: jest.fn(), showErrorMessage: jest.fn(), createStatusBarItem: jest.fn(), - withProgress: jest.fn(), + showInputBox: jest.fn(), showQuickPick: jest.fn(), + withProgress: jest.fn(), createQuickPick: jest.fn(), }, commands: { diff --git a/src/command_names.ts b/src/command_names.ts index e5c877f..d13901f 100644 --- a/src/command_names.ts +++ b/src/command_names.ts @@ -33,6 +33,7 @@ export const USER_COMMANDS = { CREATE_COMMENT: 'gl.createComment', CHECKOUT_MR_BRANCH: 'gl.checkoutMrBranch', CLONE_WIKI: 'gl.cloneWiki', + CREATE_SNIPPET_PATCH: 'gl.createSnippetPatch', }; /* diff --git a/src/commands/create_snippet.ts b/src/commands/create_snippet.ts index a025285..e56abd1 100644 --- a/src/commands/create_snippet.ts +++ b/src/commands/create_snippet.ts @@ -4,20 +4,21 @@ import * as gitLabService from '../gitlab_service'; import { gitExtensionWrapper } from '../git/git_extension_wrapper'; import { GitLabProject } from '../gitlab/gitlab_project'; -const visibilityOptions = [ - { - label: 'Public', - type: 'public', - }, - { - label: 'Internal', - type: 'internal', - }, - { - label: 'Private', - type: 'private', - }, -]; +type VisibilityItem = vscode.QuickPickItem & { type: string }; + +const PRIVATE_VISIBILITY_ITEM: VisibilityItem = { + label: '$(lock) Private', + type: 'private', + description: 'The snippet is visible only to project members.', +}; + +const PUBLIC_VISIBILITY_ITEM: VisibilityItem = { + label: '$(globe) Public', + type: 'public', + description: 'The snippet can be accessed without any authentication.', +}; + +export const VISIBILITY_OPTIONS = [PRIVATE_VISIBILITY_ITEM, PUBLIC_VISIBILITY_ITEM]; const contextOptions = [ { @@ -82,7 +83,7 @@ export async function createSnippet() { return; } - const visibility = await vscode.window.showQuickPick(visibilityOptions); + const visibility = await vscode.window.showQuickPick(VISIBILITY_OPTIONS); if (!visibility) return; const context = await vscode.window.showQuickPick(contextOptions); diff --git a/src/commands/create_snippet_patch.test.ts b/src/commands/create_snippet_patch.test.ts new file mode 100644 index 0000000..1d60418 --- /dev/null +++ b/src/commands/create_snippet_patch.test.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; +import { createSnippetPatch } from './create_snippet_patch'; +import { WrappedRepository } from '../git/wrapped_repository'; +import { project } from '../test_utils/entities'; +import { gitExtensionWrapper } from '../git/git_extension_wrapper'; +import { asMock } from '../test_utils/as_mock'; +import { createSnippet } from '../gitlab_service'; +import { openUrl } from '../openers'; + +jest.mock('../git/git_extension_wrapper'); +jest.mock('../gitlab_service'); +jest.mock('../openers'); + +const SNIPPET_URL = 'https://gitlab.com/test-group/test-project/-/snippets/2146265'; +const DIFF_OUTPUT = 'diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml'; + +describe('create snippet patch', () => { + let wrappedRepository: WrappedRepository; + + beforeEach(() => { + const mockRepository: Partial = { + lastCommitSha: 'abcd1234567', + getTrackingBranchName: async () => 'tracking-branch-name', + getProject: async () => project, + diff: async () => DIFF_OUTPUT, + }; + wrappedRepository = mockRepository as WrappedRepository; + asMock(gitExtensionWrapper.getActiveRepositoryOrSelectOne).mockResolvedValue(wrappedRepository); + asMock(vscode.window.showInputBox).mockResolvedValue('snippet_name'); + asMock(vscode.window.showQuickPick).mockImplementation(options => + options.filter((o: any) => o.type === 'private').pop(), + ); + asMock(createSnippet).mockResolvedValue({ + web_url: SNIPPET_URL, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates a snippet patch and opens it in a browser', async () => { + await createSnippetPatch(); + expect(openUrl).toHaveBeenCalledWith(SNIPPET_URL); + }); + + describe('populating the create snippet request', () => { + let formData: Record; + beforeEach(async () => { + await createSnippetPatch(); + [[, formData]] = asMock(createSnippet).mock.calls; + }); + + it('prepends "patch: " to the user input to create snippet title', () => { + expect(formData.title).toBe('patch: snippet_name'); + }); + + it('appends ".patch" to the user input to create snippet file name', () => { + expect(formData.file_name).toBe('snippet_name.patch'); + }); + + it("sets user's choice of visibility (private selected in test setup)", () => { + expect(formData.visibility).toBe('private'); + }); + + it('sets the diff command output as the blob content', () => { + expect(formData.content).toBe(DIFF_OUTPUT); + }); + }); +}); diff --git a/src/commands/create_snippet_patch.ts b/src/commands/create_snippet_patch.ts new file mode 100644 index 0000000..cc85ccb --- /dev/null +++ b/src/commands/create_snippet_patch.ts @@ -0,0 +1,63 @@ +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import * as gitLabService from '../gitlab_service'; +import * as openers from '../openers'; +import { gitExtensionWrapper } from '../git/git_extension_wrapper'; +import { VISIBILITY_OPTIONS } from './create_snippet'; + +const getSnippetPatchDescription = ( + branch: string, + commit: string, + patchFileName: string, +): string => ` +This snippet contains suggested changes for branch ${branch} (commit: ${commit}). + +Apply this snippet: + +- In VS Code with the GitLab Workflow extension installed: + - Run \`GitLab: Apply snippet patch\` and select this snippet +- Using the \`git\` command: + - Download the \`${patchFileName}\` file to your project folder + - In your project folder, run + + ~~~sh + git apply '${patchFileName}' + ~~~ + +*This snippet was created with the [GitLab Workflow VS Code extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow).* +`; + +export const createSnippetPatch = async (): Promise => { + const repository = await gitExtensionWrapper.getActiveRepositoryOrSelectOne(); + assert(repository); + assert(repository.lastCommitSha); + const patch = await repository.diff(); + const name = await vscode.window.showInputBox({ + placeHolder: 'patch name', + prompt: + 'The name is used as the snippet title and also as the filename (with .patch appended).', + }); + if (!name) return; + const visibility = await vscode.window.showQuickPick(VISIBILITY_OPTIONS); + if (!visibility) return; + + const project = await repository.getProject(); + assert(project); + const patchFileName = `${name}.patch`; + const data = { + id: project.restId, + title: `patch: ${name}`, + description: getSnippetPatchDescription( + await repository.getTrackingBranchName(), + repository.lastCommitSha, + patchFileName, + ), + file_name: patchFileName, + visibility: visibility.type, + content: patch, + }; + + const snippet = await gitLabService.createSnippet(repository.rootFsPath, data); + + await openers.openUrl(snippet.web_url); +}; diff --git a/src/extension.js b/src/extension.js index 035fb87..0a3e43e 100644 --- a/src/extension.js +++ b/src/extension.js @@ -32,6 +32,7 @@ const { changeTypeDecorationProvider } = require('./review/change_type_decoratio const { checkVersion } = require('./utils/check_version'); const { checkoutMrBranch } = require('./commands/checkout_mr_branch'); const { cloneWiki } = require('./commands/clone_wiki'); +const { createSnippetPatch } = require('./commands/create_snippet_patch'); vscode.gitLabWorkflow = { sidebarDataProviders: [], @@ -92,6 +93,7 @@ const registerCommands = (context, outputChannel) => { [USER_COMMANDS.CREATE_COMMENT]: createComment, [USER_COMMANDS.CHECKOUT_MR_BRANCH]: checkoutMrBranch, [USER_COMMANDS.CLONE_WIKI]: cloneWiki, + [USER_COMMANDS.CREATE_SNIPPET_PATCH]: createSnippetPatch, [PROGRAMMATIC_COMMANDS.NO_IMAGE_REVIEW]: () => vscode.window.showInformationMessage("GitLab MR review doesn't support images yet."), }; diff --git a/src/git/wrapped_repository.ts b/src/git/wrapped_repository.ts index c2cfa7f..c24cf40 100644 --- a/src/git/wrapped_repository.ts +++ b/src/git/wrapped_repository.ts @@ -167,6 +167,10 @@ export class WrappedRepository { return this.rawRepository.show(sha, absolutePath).catch(() => null); } + async diff(): Promise { + return this.rawRepository.diff(); + } + async getTrackingBranchName(): Promise { const branchName = this.rawRepository.state.HEAD?.name; assert( diff --git a/src/status_bar.test.ts b/src/status_bar.test.ts index b3d0575..32e520a 100644 --- a/src/status_bar.test.ts +++ b/src/status_bar.test.ts @@ -3,12 +3,11 @@ import * as gitLabService from './gitlab_service'; import { pipeline, mr, issue } from './test_utils/entities'; import { USER_COMMANDS } from './command_names'; import { gitExtensionWrapper } from './git/git_extension_wrapper'; +import { asMock } from './test_utils/as_mock'; jest.mock('./gitlab_service'); jest.mock('./git/git_extension_wrapper'); -const asMock = (mockFn: unknown) => mockFn as jest.Mock; - asMock(vscode.workspace.getConfiguration).mockReturnValue({ showStatusBarLinks: true, showIssueLinkOnStatusBar: true, diff --git a/src/test_utils/as_mock.ts b/src/test_utils/as_mock.ts new file mode 100644 index 0000000..436b790 --- /dev/null +++ b/src/test_utils/as_mock.ts @@ -0,0 +1 @@ +export const asMock = (mockFn: unknown) => mockFn as jest.Mock; -- GitLab