提交 e9d46ba0 编写于 作者: T Tomas Vik

refactor: checkout branch for MR with tests

上级 174a9557
......@@ -292,7 +292,7 @@
"view/item/context": [
{
"command": "gl.checkoutMrBranch",
"when": "view =~ /issuesAndMrs/ && viewItem == mr-item-from-branch"
"when": "view =~ /issuesAndMrs/ && viewItem == mr-item-from-same-project"
}
],
"comments/comment/title": [
......
......@@ -17,6 +17,7 @@ module.exports = {
createCommentController: jest.fn(),
},
window: {
showInformationMessage: jest.fn(),
showWarningMessage: jest.fn(),
showErrorMessage: jest.fn(),
createStatusBarItem: jest.fn(),
......
......@@ -45,4 +45,5 @@ export const PROGRAMMATIC_COMMANDS = {
export const VS_COMMANDS = {
DIFF: 'vscode.diff',
OPEN: 'vscode.open',
GIT_SHOW_OUTPUT: 'git.showOutput',
};
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';
import { WrappedRepository } from '../git/wrapped_repository';
import { mr } from '../test_utils/entities';
import { GitErrorCodes } from '../api/git';
describe('checkout MR branch', () => {
let mrItemModel: MrItemModel;
let wrappedRepository: WrappedRepository;
beforeEach(() => {
const mockRepository: Partial<WrappedRepository> = {
fetch: jest.fn().mockResolvedValue(undefined),
checkout: jest.fn().mockResolvedValue(undefined),
lastCommitSha: mr.sha,
};
wrappedRepository = mockRepository as WrappedRepository;
});
jest.mock('../sidebar.js');
vscode.window.showInformationMessage = jest.fn();
vscode.window.showErrorMessage = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
describe('MR branch commands', () => {
describe('Checkout branch by Merge request', () => {
let commandData: MrItemModel;
describe('with branch from the same project', () => {
beforeEach(() => {
const mrFromTheSameProject = {
...mr,
source_project_id: 123,
target_project_id: 123,
source_branch_name: 'feature-a',
};
mrItemModel = new MrItemModel(mrFromTheSameProject, wrappedRepository);
});
let fakeExtension: FakeGitExtension;
it('checks out the local branch', async () => {
await checkoutMrBranch(mrItemModel);
let firstWorkspace: GitLabWorkspace;
let secondWorkspace: GitLabWorkspace;
expect(wrappedRepository.fetch).toBeCalled();
expect(wrappedRepository.checkout).toBeCalledWith('feature-a');
});
let firstRepository: Repository;
let secondRepository: Repository;
it('shows a success message', async () => {
await checkoutMrBranch(mrItemModel);
beforeEach(() => {
firstWorkspace = { ...workspace };
secondWorkspace = { ...anotherWorkspace };
expect(vscode.window.showInformationMessage).toBeCalledWith('Branch changed to feature-a');
});
afterEach(() => {
(vscode.window.showInformationMessage as jest.Mock).mockReset();
(vscode.window.showWarningMessage as jest.Mock).mockReset();
it('rejects with an error if error occurred', async () => {
(wrappedRepository.checkout as jest.Mock).mockRejectedValue(new Error('error'));
await expect(checkoutMrBranch(mrItemModel)).rejects.toEqual(new Error('error'));
});
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}`]);
});
it('handles errors from the Git Extension', async () => {
(wrappedRepository.checkout as jest.Mock).mockRejectedValue({
gitErrorCode: GitErrorCodes.DirtyWorkTree,
stderr: 'Git standard output',
});
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();
});
await checkoutMrBranch(mrItemModel);
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
'Checkout failed: Git standard output',
'See Git Log',
);
});
it('warns user that their local branch is not in sync', async () => {
(wrappedRepository as any).lastCommitSha = 'abdef'; // simulate local sha being different from mr.sha
await checkoutMrBranch(mrItemModel);
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(
"Branch changed to feature-a, but it's out of sync with the remote branch. Synchronize it by pushing or pulling.",
);
});
});
describe('with branch from a forked project', () => {
beforeEach(() => {
const mrFromAFork = {
...mr,
source_project_id: 123,
target_project_id: 456,
source_branch_name: 'feature-a',
};
mrItemModel = new MrItemModel(mrFromAFork, wrappedRepository);
});
it('throws an error', async () => {
await expect(checkoutMrBranch(mrItemModel)).rejects.toMatchObject({
message: 'this command is only available for same-project MRs',
});
});
});
......
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';
import { VS_COMMANDS } from '../command_names';
import { doNotAwait } from '../utils/do_not_await';
const handleGitError = async (e: { stderr: string }) => {
const SEE_GIT_LOG = 'See Git Log';
const choice = await vscode.window.showErrorMessage(`Checkout failed: ${e.stderr}`, SEE_GIT_LOG);
if (choice === SEE_GIT_LOG) {
await vscode.commands.executeCommand(VS_COMMANDS.GIT_SHOW_OUTPUT);
}
};
/**
* 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
* Command will checkout source branch for merge request. Merge request must be from local branch.
*/
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;
export const checkoutMrBranch = async (mrItemModel: MrItemModel): Promise<void> => {
const { mr } = mrItemModel;
assert(
mr.target_project_id === mr.source_project_id,
'this command is only available for same-project MRs',
);
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 { repository } = mrItemModel;
doNotAwait(vscode.window.showInformationMessage('Fetching branches...'));
await repository.fetch();
await repository.checkout(mr.source_branch);
if (repository.lastCommitSha !== mr.sha) {
await vscode.window.showWarningMessage(
`Branch changed to ${mr.source_branch}, but it's out of sync with the remote branch. Synchronize it by pushing or pulling.`,
);
return;
}
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}`);
await vscode.window.showInformationMessage(`Branch changed to ${mr.source_branch}`);
} catch (e) {
handleError(
new UserFriendlyError(
e.stderr || `${(e as Error).message}` || `Couldn't checkout branch ${sourceBranchName}`,
e,
),
);
if (e.gitErrorCode) {
await handleGitError(e);
return;
}
throw e;
}
};
......@@ -79,6 +79,25 @@ export class WrappedRepository {
return preferredRemote || branchRemote || firstRemote || 'origin';
}
async fetch(): Promise<void> {
await this.rawRepository.fetch();
}
async checkout(branchName: string): Promise<void> {
await this.rawRepository.checkout(branchName);
assert(
this.rawRepository.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 = this.rawRepository.state.HEAD.name;
assert(
currentBranchName === branchName,
`The branch name after the checkout (${currentBranchName}) is not the branch that the extension tried to check out (${branchName}). Inspect your repository before making any more changes.`,
);
}
getRemoteByName(remoteName: string): GitRemote {
const remoteUrl = this.rawRepository.state.remotes.find(r => r.name === remoteName)?.fetchUrl;
assert(remoteUrl, `could not find any URL for git remote with name '${this.remoteName}'`);
......
......@@ -33,7 +33,6 @@ export const mr: RestMr = {
source_project_id: 9999,
target_project_id: 9999,
source_branch: 'feature-a',
target_branch: 'main',
};
export const diffFile: RestDiffFile = {
......
......@@ -25,7 +25,6 @@ interface RestMr extends RestIssuable {
sha: string;
source_project_id: number;
target_project_id: number;
target_branch: string;
source_branch: string;
}
......
/** doNotAwait is used to circumvent the otherwise invaluable
* @typescript-eslint/no-floating-promises rule. This util is meant
* for informative messages that would otherwise block execution */
export const doNotAwait = (promise: Thenable<any>): void => {
// not waiting for the promise
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册