Commit d0152079 authored by Lennard Sprong's avatar Lennard Sprong Committed by Elwyn Benson
Browse files

feat: Ask to push branch before creating MR

parent 28efcb20
Loading
Loading
Loading
Loading
+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.
});
+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,
    )}`,
  );
};
+0 −11
Original line number Diff line number Diff line
@@ -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);
};
+2 −1
Original line number Diff line number Diff line
@@ -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,
@@ -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),
+16 −1
Original line number Diff line number Diff line
@@ -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';

@@ -19,6 +22,8 @@ export interface FakeRepositoryOptions {
  remotes: [string, string?, string?][];
  headName?: string;
  headRemoteName?: string;
  upstream?: UpstreamRef;
  hasLocalChanges?: boolean;
  commit?: string;
}

@@ -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,
  };
@@ -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,