Commit 174a9557 authored by Tomas Vik (OOO back on 2026-08-31)'s avatar Tomas Vik (OOO back on 2026-08-31) 🌴
Browse files

feat(view issues-and-mrs): checkout local branch for merge request

Credit: [@Musisimaru](https://gitlab.com/Musisimaru) (originally
    introduced in
    [!229](gitlab-org/gitlab-vscode-extension!229))

This is a copy paste of the code contributed by Mussimaru in
gitlab-org/gitlab-vscode-extension!229
This code doesn't compile, but I wanted to include it as a record that
he's done most of the work.
parent 34f55649
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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",
+1 −0
Original line number Diff line number Diff line
@@ -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',
};

/*
+101 −0
Original line number Diff line number Diff line
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();
        });
      });
    });
  });
});
+56 −0
Original line number Diff line number Diff line
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<void> => {
  // 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,
      ),
    );
  }
};
+2 −0
Original line number Diff line number Diff line
@@ -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."),
  };
Loading