Loading src/desktop/commands/create_mr.test.ts 0 → 100644 +83 −0 Original line number Diff line number Diff line import path from 'path'; import * as vscode from 'vscode'; import { GitRepository } from '../git/new_git'; import { ProjectInRepository } from '../gitlab/new_project'; import * as testEntities from '../test_utils/entities'; import { createFakeRepository } from '../test_utils/fake_git_extension'; import { Repository } from '../api/git'; import { VS_COMMANDS } from '../../common/command_names'; import { openCreateNewMr } from './create_mr'; function createProjectInRepository(rawRepository: Repository): ProjectInRepository { const repoRootPath = path.join('path', 'to', 'repo'); return { ...testEntities.projectInRepository, pointer: { ...testEntities.projectInRepository.pointer, repository: { rootFsPath: repoRootPath, rawRepository, } as GitRepository, }, }; } describe('openCreateNewMr', () => { beforeEach(() => { (vscode.window.showWarningMessage as jest.Mock).mockImplementation((title, details, option) => { return option; }); (vscode.window.withProgress as jest.Mock).mockImplementation((_, task) => task()); }); afterEach(() => { jest.resetAllMocks(); }); it('opens the browser when upstream exists', async () => { const rawRepository = createFakeRepository({ headRemoteName: 'branch', upstream: { name: 'yes', remote: 'remote', }, }); const pir = createProjectInRepository(rawRepository); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).not.toHaveBeenCalled(); expect(vscode.commands.executeCommand).toHaveBeenCalledWith( VS_COMMANDS.OPEN, vscode.Uri.parse( `https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/new?merge_request%5Bsource_branch%5D=branch`, ), ); }); it('offers to push when local repository is clean', async () => { const rawRepository = createFakeRepository({ headName: 'branch' }); const pir = createProjectInRepository(rawRepository); expect(rawRepository.state.HEAD?.remote).toBeUndefined(); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).toHaveBeenCalled(); expect(rawRepository.state.HEAD?.remote).toBeDefined(); }); it('offers to go to the sidebar when local repository is not clean', async () => { const rawRepository = createFakeRepository({ headName: 'branch', hasLocalChanges: true }); const pir = createProjectInRepository(rawRepository); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).toHaveBeenCalled(); expect(vscode.commands.executeCommand).toHaveBeenCalledWith('workbench.view.scm'); }); // TODO add tests for missing tracking branch etc. }); src/desktop/commands/create_mr.ts 0 → 100644 +76 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { isEmpty } from 'lodash'; import { Repository } from '../api/git'; import { USER_COMMANDS } from '../command_names'; import { GitRemoteUrlPointer } from '../git/new_git'; import { DetachedHeadError } from '../errors/detached_head_error'; import { getTrackingBranchName } from '../git/get_tracking_branch_name'; import { ProjectCommand } from './run_with_valid_project'; import { openUrl } from './openers'; async function pushBranchForMergeRequest(pointer: GitRemoteUrlPointer) { const { rawRepository } = pointer.repository; const { state } = rawRepository; const isClean = isEmpty(state.indexChanges) && isEmpty(state.workingTreeChanges); let message = 'You must push this branch to GitLab before you can create a merge request.'; if (!isClean) { message += '\n\nYou have uncommitted changes. Use the Source Control tab to commit them first.'; } if ( !(await vscode.window.showWarningMessage( 'Push branch to GitLab', { modal: true, detail: message, }, isClean ? 'Publish Branch' : 'Open Source Control', )) ) { return; } if (!isClean) { await vscode.commands.executeCommand('workbench.view.scm'); return; } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Pushing to GitLab…', }, () => rawRepository.push(pointer.remote.name, state.HEAD?.name, true), ); if ( await vscode.window.showInformationMessage('Branch pushed to GitLab.', 'Create merge request') ) { await vscode.commands.executeCommand(USER_COMMANDS.OPEN_CREATE_NEW_MR); } } async function getTrackingBranchNameOrThrow(rawRepository: Repository): Promise<string> { const branchName = await getTrackingBranchName(rawRepository); if (!branchName) throw new DetachedHeadError(); return branchName; } export const openCreateNewMr: ProjectCommand = async projectInRepository => { const { project, pointer } = projectInRepository; const branch = pointer.repository.rawRepository.state.HEAD; if (branch?.name && !branch.upstream) { await pushBranchForMergeRequest(pointer); return; } const branchName = await getTrackingBranchNameOrThrow(pointer.repository.rawRepository); await openUrl( `${project.webUrl}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent( branchName, )}`, ); }; src/desktop/commands/openers.ts +0 −11 Original line number Diff line number Diff line Loading @@ -143,17 +143,6 @@ export const openCreateNewIssue: ProjectCommand = async projectInRepository => { await openTemplatedLink('$projectUrl/-/issues/new', projectInRepository); }; export const openCreateNewMr: ProjectCommand = async projectInRepository => { const { project, pointer } = projectInRepository; const branchName = await getTrackingBranchNameOrThrow(pointer.repository.rawRepository); await openUrl( `${project.webUrl}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent( branchName, )}`, ); }; export const openProjectPage: ProjectCommand = async projectInRepository => { await openTemplatedLink('$projectUrl', projectInRepository); }; Loading src/desktop/extension.ts +2 −1 Original line number Diff line number Diff line Loading @@ -178,6 +178,7 @@ import { logNetworkConfiguration } from './gitlab/http/log_network_options'; import { IssuableDataProviderFactory } from './tree_view/issuable_data_provider_factory'; import { getClientContext } from './language_server/get_client_context'; import { openFlowBuilderCommand } from './commands/open_flow_builder'; import { openCreateNewMr } from './commands/create_mr'; const registerCommands = ( context: vscode.ExtensionContext, Loading Loading @@ -209,7 +210,7 @@ const registerCommands = ( ), [USER_COMMANDS.OPEN_COMMIT_IN_GITLAB]: openers.openCommitInGitLab, [USER_COMMANDS.OPEN_CREATE_NEW_ISSUE]: runWithValidProject(openers.openCreateNewIssue), [USER_COMMANDS.OPEN_CREATE_NEW_MR]: runWithValidProject(openers.openCreateNewMr), [USER_COMMANDS.OPEN_CREATE_NEW_MR]: runWithValidProject(openCreateNewMr), [USER_COMMANDS.OPEN_PROJECT_PAGE]: runWithValidProject(openers.openProjectPage), [USER_COMMANDS.PIPELINE_ACTIONS]: runWithValidProject(triggerPipelineAction), [USER_COMMANDS.ISSUE_SEARCH]: runWithValidProject(showIssueSearchInput), Loading src/desktop/test_utils/fake_git_extension.ts +16 −1 Original line number Diff line number Diff line Loading @@ -2,12 +2,15 @@ import * as vscode from 'vscode'; import { API, Change, CredentialsProvider, RefType, RemoteSourceProvider, RemoteSourcePublisher, Repository, SourceControlHistoryItemDetailsProvider, Status, UpstreamRef, } from '../api/git'; import { EventEmitter } from './event_emitter'; Loading @@ -19,6 +22,8 @@ export interface FakeRepositoryOptions { remotes: [string, string?, string?][]; headName?: string; headRemoteName?: string; upstream?: UpstreamRef; hasLocalChanges?: boolean; commit?: string; } Loading @@ -26,8 +31,16 @@ export const fakeRepositoryOptions: FakeRepositoryOptions = { rootUriPath: '/path/to/repo', remotes: [['origin', 'git@a.com:gitlab/extension.git']], }; const fakeChange: Change = { uri: vscode.Uri.file('/path/to/repo'), originalUri: vscode.Uri.file('/path/to/repo'), status: Status.INDEX_MODIFIED, renameUri: undefined, }; export const createFakeRepository = (options: Partial<FakeRepositoryOptions> = {}): Repository => { const { rootUriPath, remotes, headName, headRemoteName, commit } = { const { rootUriPath, remotes, headName, headRemoteName, commit, upstream, hasLocalChanges } = { ...fakeRepositoryOptions, ...options, }; Loading @@ -42,8 +55,10 @@ export const createFakeRepository = (options: Partial<FakeRepositoryOptions> = { type: RefType.Head, remote: headRemoteName, name: headName ?? headRemoteName, upstream, commit, }, indexChanges: hasLocalChanges ? [fakeChange] : [], refs: [], onDidChangeEmitter, onDidChange: onDidChangeEmitter.event, Loading Loading
src/desktop/commands/create_mr.test.ts 0 → 100644 +83 −0 Original line number Diff line number Diff line import path from 'path'; import * as vscode from 'vscode'; import { GitRepository } from '../git/new_git'; import { ProjectInRepository } from '../gitlab/new_project'; import * as testEntities from '../test_utils/entities'; import { createFakeRepository } from '../test_utils/fake_git_extension'; import { Repository } from '../api/git'; import { VS_COMMANDS } from '../../common/command_names'; import { openCreateNewMr } from './create_mr'; function createProjectInRepository(rawRepository: Repository): ProjectInRepository { const repoRootPath = path.join('path', 'to', 'repo'); return { ...testEntities.projectInRepository, pointer: { ...testEntities.projectInRepository.pointer, repository: { rootFsPath: repoRootPath, rawRepository, } as GitRepository, }, }; } describe('openCreateNewMr', () => { beforeEach(() => { (vscode.window.showWarningMessage as jest.Mock).mockImplementation((title, details, option) => { return option; }); (vscode.window.withProgress as jest.Mock).mockImplementation((_, task) => task()); }); afterEach(() => { jest.resetAllMocks(); }); it('opens the browser when upstream exists', async () => { const rawRepository = createFakeRepository({ headRemoteName: 'branch', upstream: { name: 'yes', remote: 'remote', }, }); const pir = createProjectInRepository(rawRepository); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).not.toHaveBeenCalled(); expect(vscode.commands.executeCommand).toHaveBeenCalledWith( VS_COMMANDS.OPEN, vscode.Uri.parse( `https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/merge_requests/new?merge_request%5Bsource_branch%5D=branch`, ), ); }); it('offers to push when local repository is clean', async () => { const rawRepository = createFakeRepository({ headName: 'branch' }); const pir = createProjectInRepository(rawRepository); expect(rawRepository.state.HEAD?.remote).toBeUndefined(); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).toHaveBeenCalled(); expect(rawRepository.state.HEAD?.remote).toBeDefined(); }); it('offers to go to the sidebar when local repository is not clean', async () => { const rawRepository = createFakeRepository({ headName: 'branch', hasLocalChanges: true }); const pir = createProjectInRepository(rawRepository); await openCreateNewMr(pir); expect(vscode.window.showWarningMessage).toHaveBeenCalled(); expect(vscode.commands.executeCommand).toHaveBeenCalledWith('workbench.view.scm'); }); // TODO add tests for missing tracking branch etc. });
src/desktop/commands/create_mr.ts 0 → 100644 +76 −0 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { isEmpty } from 'lodash'; import { Repository } from '../api/git'; import { USER_COMMANDS } from '../command_names'; import { GitRemoteUrlPointer } from '../git/new_git'; import { DetachedHeadError } from '../errors/detached_head_error'; import { getTrackingBranchName } from '../git/get_tracking_branch_name'; import { ProjectCommand } from './run_with_valid_project'; import { openUrl } from './openers'; async function pushBranchForMergeRequest(pointer: GitRemoteUrlPointer) { const { rawRepository } = pointer.repository; const { state } = rawRepository; const isClean = isEmpty(state.indexChanges) && isEmpty(state.workingTreeChanges); let message = 'You must push this branch to GitLab before you can create a merge request.'; if (!isClean) { message += '\n\nYou have uncommitted changes. Use the Source Control tab to commit them first.'; } if ( !(await vscode.window.showWarningMessage( 'Push branch to GitLab', { modal: true, detail: message, }, isClean ? 'Publish Branch' : 'Open Source Control', )) ) { return; } if (!isClean) { await vscode.commands.executeCommand('workbench.view.scm'); return; } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Pushing to GitLab…', }, () => rawRepository.push(pointer.remote.name, state.HEAD?.name, true), ); if ( await vscode.window.showInformationMessage('Branch pushed to GitLab.', 'Create merge request') ) { await vscode.commands.executeCommand(USER_COMMANDS.OPEN_CREATE_NEW_MR); } } async function getTrackingBranchNameOrThrow(rawRepository: Repository): Promise<string> { const branchName = await getTrackingBranchName(rawRepository); if (!branchName) throw new DetachedHeadError(); return branchName; } export const openCreateNewMr: ProjectCommand = async projectInRepository => { const { project, pointer } = projectInRepository; const branch = pointer.repository.rawRepository.state.HEAD; if (branch?.name && !branch.upstream) { await pushBranchForMergeRequest(pointer); return; } const branchName = await getTrackingBranchNameOrThrow(pointer.repository.rawRepository); await openUrl( `${project.webUrl}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent( branchName, )}`, ); };
src/desktop/commands/openers.ts +0 −11 Original line number Diff line number Diff line Loading @@ -143,17 +143,6 @@ export const openCreateNewIssue: ProjectCommand = async projectInRepository => { await openTemplatedLink('$projectUrl/-/issues/new', projectInRepository); }; export const openCreateNewMr: ProjectCommand = async projectInRepository => { const { project, pointer } = projectInRepository; const branchName = await getTrackingBranchNameOrThrow(pointer.repository.rawRepository); await openUrl( `${project.webUrl}/-/merge_requests/new?merge_request%5Bsource_branch%5D=${encodeURIComponent( branchName, )}`, ); }; export const openProjectPage: ProjectCommand = async projectInRepository => { await openTemplatedLink('$projectUrl', projectInRepository); }; Loading
src/desktop/extension.ts +2 −1 Original line number Diff line number Diff line Loading @@ -178,6 +178,7 @@ import { logNetworkConfiguration } from './gitlab/http/log_network_options'; import { IssuableDataProviderFactory } from './tree_view/issuable_data_provider_factory'; import { getClientContext } from './language_server/get_client_context'; import { openFlowBuilderCommand } from './commands/open_flow_builder'; import { openCreateNewMr } from './commands/create_mr'; const registerCommands = ( context: vscode.ExtensionContext, Loading Loading @@ -209,7 +210,7 @@ const registerCommands = ( ), [USER_COMMANDS.OPEN_COMMIT_IN_GITLAB]: openers.openCommitInGitLab, [USER_COMMANDS.OPEN_CREATE_NEW_ISSUE]: runWithValidProject(openers.openCreateNewIssue), [USER_COMMANDS.OPEN_CREATE_NEW_MR]: runWithValidProject(openers.openCreateNewMr), [USER_COMMANDS.OPEN_CREATE_NEW_MR]: runWithValidProject(openCreateNewMr), [USER_COMMANDS.OPEN_PROJECT_PAGE]: runWithValidProject(openers.openProjectPage), [USER_COMMANDS.PIPELINE_ACTIONS]: runWithValidProject(triggerPipelineAction), [USER_COMMANDS.ISSUE_SEARCH]: runWithValidProject(showIssueSearchInput), Loading
src/desktop/test_utils/fake_git_extension.ts +16 −1 Original line number Diff line number Diff line Loading @@ -2,12 +2,15 @@ import * as vscode from 'vscode'; import { API, Change, CredentialsProvider, RefType, RemoteSourceProvider, RemoteSourcePublisher, Repository, SourceControlHistoryItemDetailsProvider, Status, UpstreamRef, } from '../api/git'; import { EventEmitter } from './event_emitter'; Loading @@ -19,6 +22,8 @@ export interface FakeRepositoryOptions { remotes: [string, string?, string?][]; headName?: string; headRemoteName?: string; upstream?: UpstreamRef; hasLocalChanges?: boolean; commit?: string; } Loading @@ -26,8 +31,16 @@ export const fakeRepositoryOptions: FakeRepositoryOptions = { rootUriPath: '/path/to/repo', remotes: [['origin', 'git@a.com:gitlab/extension.git']], }; const fakeChange: Change = { uri: vscode.Uri.file('/path/to/repo'), originalUri: vscode.Uri.file('/path/to/repo'), status: Status.INDEX_MODIFIED, renameUri: undefined, }; export const createFakeRepository = (options: Partial<FakeRepositoryOptions> = {}): Repository => { const { rootUriPath, remotes, headName, headRemoteName, commit } = { const { rootUriPath, remotes, headName, headRemoteName, commit, upstream, hasLocalChanges } = { ...fakeRepositoryOptions, ...options, }; Loading @@ -42,8 +55,10 @@ export const createFakeRepository = (options: Partial<FakeRepositoryOptions> = { type: RefType.Head, remote: headRemoteName, name: headName ?? headRemoteName, upstream, commit, }, indexChanges: hasLocalChanges ? [fakeChange] : [], refs: [], onDidChangeEmitter, onDidChange: onDidChangeEmitter.event, Loading