From 174a95575ca85e9db054c3ddfbf882c755cc309a Mon Sep 17 00:00:00 2001 From: Tomas Vik Date: Tue, 22 Jun 2021 10:47:58 +0200 Subject: [PATCH] feat(view issues-and-mrs): checkout local branch for merge request Credit: [@Musisimaru](https://gitlab.com/Musisimaru) (originally introduced in [!229](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/229)) This is a copy paste of the code contributed by Mussimaru in https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/229 This code doesn't compile, but I wanted to include it as a record that he's done most of the work. --- package.json | 14 ++++ src/command_names.ts | 1 + src/commands/checkout_mr_branch.test.ts | 101 ++++++++++++++++++++++++ src/commands/checkout_mr_branch.ts | 56 +++++++++++++ src/extension.js | 2 + src/test_utils/entities.ts | 2 + src/types.d.ts | 2 + 7 files changed, 178 insertions(+) create mode 100644 src/commands/checkout_mr_branch.test.ts create mode 100644 src/commands/checkout_mr_branch.ts diff --git a/package.json b/package.json index 6945d8d..750a46d 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,10 @@ "command": "gl.createComment", "title": "Add comment now", "category": "GitLab" + }, + { + "command": "gl.checkoutMrBranch", + "title": "Checkout MR branch" } ], "menus": { @@ -201,6 +205,10 @@ "command": "gl.createComment", "when": "false" }, + { + "command": "gl.checkoutMrBranch", + "when": "false" + }, { "command": "gl.showIssuesAssignedToMe", "when": "gitlab:validState" @@ -281,6 +289,12 @@ "group": "navigation" } ], + "view/item/context": [ + { + "command": "gl.checkoutMrBranch", + "when": "view =~ /issuesAndMrs/ && viewItem == mr-item-from-branch" + } + ], "comments/comment/title": [ { "command": "gl.startEditingComment", diff --git a/src/command_names.ts b/src/command_names.ts index b1bce6d..581d604 100644 --- a/src/command_names.ts +++ b/src/command_names.ts @@ -31,6 +31,7 @@ export const USER_COMMANDS = { CANCEL_EDITING_COMMENT: 'gl.cancelEditingComment', SUBMIT_COMMENT_EDIT: 'gl.submitCommentEdit', CREATE_COMMENT: 'gl.createComment', + CHECKOUT_MR_BRANCH: 'gl.checkoutMrBranch', }; /* diff --git a/src/commands/checkout_mr_branch.test.ts b/src/commands/checkout_mr_branch.test.ts new file mode 100644 index 0000000..8ada818 --- /dev/null +++ b/src/commands/checkout_mr_branch.test.ts @@ -0,0 +1,101 @@ +import * as vscode from 'vscode'; +import * as sidebar from '../sidebar.js'; +import { GitExtension, Repository } from '../api/git'; +import { MrItemModel } from '../data_providers/items/mr_item_model'; +import { anotherWorkspace, mr, workspace } from '../test_utils/entities'; +import { createFakeRepository, FakeGitExtension } from '../test_utils/fake_git_extension'; +import { checkoutMrBranch } from './checkout_mr_branch'; +import { gitExtensionWrapper } from '../git/git_extension_wrapper'; + +jest.mock('../sidebar.js'); +vscode.window.showInformationMessage = jest.fn(); +vscode.window.showErrorMessage = jest.fn(); + +describe('MR branch commands', () => { + describe('Checkout branch by Merge request', () => { + let commandData: MrItemModel; + + let fakeExtension: FakeGitExtension; + + let firstWorkspace: GitLabWorkspace; + let secondWorkspace: GitLabWorkspace; + + let firstRepository: Repository; + let secondRepository: Repository; + + beforeEach(() => { + firstWorkspace = { ...workspace }; + secondWorkspace = { ...anotherWorkspace }; + }); + + afterEach(() => { + (vscode.window.showInformationMessage as jest.Mock).mockReset(); + (vscode.window.showWarningMessage as jest.Mock).mockReset(); + }); + + describe('If merge request from local branch', () => { + describe('Basic functionality', () => { + beforeEach(() => { + firstRepository = createFakeRepository(firstWorkspace.uri); + secondRepository = createFakeRepository(secondWorkspace.uri); + + fakeExtension = new FakeGitExtension(firstRepository, secondRepository); + jest + .spyOn(gitExtensionWrapper, 'Extension', 'get') + .mockReturnValue((fakeExtension as unknown) as GitExtension); + + commandData = new MrItemModel(mr, firstWorkspace); + + checkoutByMRFromLocalBranch(commandData); + }); + + it('(local-branch) Branch fetch message', () => { + expect((vscode.window.showInformationMessage as jest.Mock).mock.calls[0]).toEqual([ + 'Fetching branches...', + ]); + }); + + it('(local-branch) Was checkout', async () => { + await expect(firstRepository.checkout).toBeCalled(); + }); + + it('(local-branch) Was fetching before checkout', async () => { + await expect(firstRepository.checkout).toBeCalled(); + expect(firstRepository.fetch).toBeCalled(); + }); + + it('(local-branch) There were no error messages', () => { + expect(vscode.window.showErrorMessage).not.toBeCalled(); + }); + + it('(local-branch) Sidebar was refreshed', () => { + expect(sidebar.refresh).toBeCalled(); + }); + + it('(local-branch) Message about success', () => { + const callsCount = (vscode.window.showInformationMessage as jest.Mock).mock.calls.length; + expect( + (vscode.window.showInformationMessage as jest.Mock).mock.calls[callsCount - 1], + ).toEqual([`Branch successfully changed to ${mr.source_branch}`]); + }); + }); + describe('Multi-root Workspaces', () => { + beforeEach(() => { + commandData = new MrItemModel(mr, secondWorkspace); + + checkoutByMRFromLocalBranch(commandData); + }); + + it('(multi-root) The branch was checkout from right repository', async () => { + await expect(secondRepository.checkout).toBeCalled(); + }); + + it('(multi-root) The branch from second repository was not checkout', async () => { + await expect(secondRepository.checkout).toBeCalled(); + expect(firstRepository.fetch).not.toBeCalled(); + expect(firstRepository.checkout).not.toBeCalled(); + }); + }); + }); + }); +}); diff --git a/src/commands/checkout_mr_branch.ts b/src/commands/checkout_mr_branch.ts new file mode 100644 index 0000000..a1d9d79 --- /dev/null +++ b/src/commands/checkout_mr_branch.ts @@ -0,0 +1,56 @@ +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import * as sidebar from '../sidebar.js'; +import { MrItemModel } from '../data_providers/items/mr_item_model'; +import { gitExtensionWrapper } from '../git/git_extension_wrapper'; +import { UserFriendlyError } from '../errors/user_friendly_error'; +import { handleError } from '../log'; + +/** + * Command will checkout source branch by merge request in current git. Merge request must be from local branch. + * @param data item of view from the left sidebar + */ +export const checkoutMrBranch = async (data: MrItemModel): Promise => { + // todo: Change data.workspace to data.repository (issue #345) + assert(data.mr.target_project_id === data.mr.source_project_id); + const { showInformationMessage } = vscode.window; + const sourceBranchName = data.mr.source_branch as string; + let branchNameForCheckout: string; + try { + const repos = gitExtensionWrapper.getRepositoriesByWorkspace(data.workspace); + if (repos.length > 1) { + throw new Error( + `You have more then one repos in one workspace. Extension can't work with this case yet. But we will fix it on soon.`, + ); + } + const repo = repos[0]; + showInformationMessage('Fetching branches...'); + + // merge from local branch + branchNameForCheckout = sourceBranchName; + await repo.fetch(); + await repo.checkout(sourceBranchName); + + assert( + repo.state.HEAD, + "We can't read repository HEAD. We suspect that your `git head` command fails and we can't continue till it succeeds", + ); + + const currentBranchName = repo.state.HEAD.name; + if (currentBranchName !== branchNameForCheckout) { + throw new Error( + `The branch name after the checkout (${currentBranchName}) is not the branch that the extension tried to check out (${branchNameForCheckout}). This is an unexpected error, please inspect your repository before making any further changes.`, + ); + } + + sidebar.refresh(); + showInformationMessage(`Branch successfully changed to ${sourceBranchName}`); + } catch (e) { + handleError( + new UserFriendlyError( + e.stderr || `${(e as Error).message}` || `Couldn't checkout branch ${sourceBranchName}`, + e, + ), + ); + } +}; diff --git a/src/extension.js b/src/extension.js index 367529b..09b7d31 100644 --- a/src/extension.js +++ b/src/extension.js @@ -29,6 +29,7 @@ const { } = require('./commands/mr_discussion_commands'); const { fileDecorationProvider } = require('./review/file_decoration_provider'); const { checkVersion } = require('./utils/check_version'); +const { checkoutMrBranch } = require('./commands/checkout_mr_branch'); vscode.gitLabWorkflow = { sidebarDataProviders: [], @@ -87,6 +88,7 @@ const registerCommands = (context, outputChannel) => { [USER_COMMANDS.CANCEL_EDITING_COMMENT]: cancelEdit, [USER_COMMANDS.SUBMIT_COMMENT_EDIT]: submitEdit, [USER_COMMANDS.CREATE_COMMENT]: createComment, + [USER_COMMANDS.CHECKOUT_MR_BRANCH]: checkoutMrBranch, [PROGRAMMATIC_COMMANDS.NO_IMAGE_REVIEW]: () => vscode.window.showInformationMessage("GitLab MR review doesn't support images yet."), }; diff --git a/src/test_utils/entities.ts b/src/test_utils/entities.ts index a9d7f91..c87c89e 100644 --- a/src/test_utils/entities.ts +++ b/src/test_utils/entities.ts @@ -32,6 +32,8 @@ export const mr: RestMr = { sha: '69ad609e8891b8aa3db85a35cd2c5747705bd76a', source_project_id: 9999, target_project_id: 9999, + source_branch: 'feature-a', + target_branch: 'main', }; export const diffFile: RestDiffFile = { diff --git a/src/types.d.ts b/src/types.d.ts index ae4f844..b2ff705 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -25,6 +25,8 @@ interface RestMr extends RestIssuable { sha: string; source_project_id: number; target_project_id: number; + target_branch: string; + source_branch: string; } interface RestMrVersion { -- GitLab