Commit b102fd67 authored by Tom Quirk's avatar Tom Quirk Committed by Jacques Erasmus

Add Jira Connect branches controller

Adds :new route for the ability to create branches from Jira issues via
the GitLab for Jira app.

Also expands the app_descriptor to specify the create branch redirect
URL, if the feature flag is enabled.

Changelog: added
Co-authored-by: default avatarMarkus Koller <mkoller@gitlab.com>
parent 9fdc576c
......@@ -41,7 +41,7 @@ export default {
};
},
update(data) {
return data?.projects?.nodes.filter((project) => !project.repository.empty) ?? [];
return data?.projects?.nodes.filter((project) => !project.repository?.empty) ?? [];
},
result() {
this.initialProjectsLoading = false;
......
import initJiraConnectBranches from '~/jira_connect/branches';
initJiraConnectBranches();
......@@ -44,27 +44,14 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
def modules
modules = {
jiraDevelopmentTool: {
key: 'gitlab-development-tool',
application: {
value: 'GitLab'
},
name: {
value: 'GitLab'
},
url: HOME_URL,
logoUrl: logo_url,
capabilities: %w(branch commit pull_request)
},
postInstallPage: {
key: 'gitlab-configuration',
name: {
value: 'GitLab Configuration'
},
name: { value: 'GitLab Configuration' },
url: relative_to_base_path(jira_connect_subscriptions_path)
}
}
modules.merge!(development_tool_module)
modules.merge!(build_information_module)
modules.merge!(deployment_information_module)
modules.merge!(feature_flag_module)
......@@ -76,6 +63,29 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
view_context.image_url('gitlab_logo.png')
end
# See https://developer.atlassian.com/cloud/jira/software/modules/development-tool/
def development_tool_module
actions = {}
if JiraConnect::BranchesController.feature_enabled?(current_user)
actions[:createBranch] = {
templateUrl: new_jira_connect_branch_url + '?issue_key={issue.key}&issue_summary={issue.summary}'
}
end
{
jiraDevelopmentTool: {
actions: actions,
key: 'gitlab-development-tool',
application: { value: 'GitLab' },
name: { value: 'GitLab' },
url: HOME_URL,
logoUrl: logo_url,
capabilities: %w(branch commit pull_request)
}
}
end
# See: https://developer.atlassian.com/cloud/jira/software/modules/deployment/
def deployment_information_module
{
......@@ -92,9 +102,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
{
jiraFeatureFlagInfoProvider: common_module_properties.merge(
actions: {}, # TODO: create, link and list feature flags https://gitlab.com/gitlab-org/gitlab/-/issues/297386
name: {
value: 'GitLab Feature Flags'
},
name: { value: 'GitLab Feature Flags' },
key: 'gitlab-feature-flags'
)
}
......
# frozen_string_literal: true
# NOTE: This controller does not inherit from JiraConnect::ApplicationController
# because we don't receive a JWT for this action, so we rely on standard GitLab authentication.
class JiraConnect::BranchesController < ApplicationController
before_action :feature_enabled!
feature_category :integrations
def new
return unless params[:issue_key].present?
@branch_name = Issue.to_branch_name(
params[:issue_key],
params[:issue_summary]
)
end
def self.feature_enabled?(user)
Feature.enabled?(:jira_connect_create_branch, user, default_enabled: :yaml)
end
private
def feature_enabled!
render_404 unless self.class.feature_enabled?(current_user)
end
end
......@@ -317,6 +317,21 @@ class Issue < ApplicationRecord
)
end
def self.to_branch_name(*args)
branch_name = args.map(&:to_s).each_with_index.map do |arg, i|
arg.parameterize(preserve_case: i == 0).presence
end.compact.join('-')
if branch_name.length > 100
truncated_string = branch_name[0, 100]
# Delete everything dangling after the last hyphen so as not to risk
# existence of unintended words in the branch name due to mid-word split.
branch_name = truncated_string.sub(/-[^-]*\Z/, '')
end
branch_name
end
# Temporary disable moving null elements because of performance problems
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
def check_repositioning_allowed!
......@@ -384,16 +399,7 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
branch_name = "#{iid}-#{title.parameterize}"
if branch_name.length > 100
truncated_string = branch_name[0, 100]
# Delete everything dangling after the last hyphen so as not to risk
# existence of unintended words in the branch name due to mid-word split.
branch_name = truncated_string[0, truncated_string.rindex("-")]
end
branch_name
self.class.to_branch_name(iid, title)
end
end
......
- @hide_breadcrumbs = true
- @hide_top_links = true
- page_title _('New branch')
.js-jira-connect-create-branch{ data: { initial_branch_name: @branch_name } }
---
name: jira_connect_create_branch
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66032
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336191
milestone: '14.2'
type: development
group: group::ecosystem
default_enabled: false
......@@ -13,4 +13,5 @@ namespace :jira_connect do
end
resources :subscriptions, only: [:index, :create, :destroy]
resources :branches, only: [:new]
end
......@@ -81,7 +81,7 @@ module Atlassian
end
def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil, update_sequence_id: nil)
repo = Serializers::RepositoryEntity.represent(
repo = ::Atlassian::JiraConnect::Serializers::RepositoryEntity.represent(
project,
commits: commits,
branches: branches,
......
......@@ -4,20 +4,100 @@ require 'spec_helper'
RSpec.describe JiraConnect::AppDescriptorController do
describe '#show' do
let(:descriptor) do
json_response.deep_symbolize_keys
end
let(:logo_url) { %r{\Ahttp://test\.host/assets/gitlab_logo-\h+\.png\z} }
let(:common_module_properties) do
{
homeUrl: 'https://gitlab.com',
logoUrl: logo_url,
documentationUrl: 'https://docs.gitlab.com/ee/integration/jira/'
}
end
it 'returns JSON app descriptor' do
get :show
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
'baseUrl' => 'https://test.host/-/jira_connect',
'lifecycle' => {
'installed' => '/events/installed',
'uninstalled' => '/events/uninstalled'
expect(descriptor).to include(
name: Atlassian::JiraConnect.app_name,
description: kind_of(String),
key: Atlassian::JiraConnect.app_key,
baseUrl: 'https://test.host/-/jira_connect',
lifecycle: {
installed: '/events/installed',
uninstalled: '/events/uninstalled'
},
vendor: {
name: 'GitLab',
url: 'https://gitlab.com'
},
links: {
documentation: 'http://test.host/help/integration/jira_development_panel#gitlabcom-1'
},
authentication: {
type: 'jwt'
},
'links' => {
'documentation' => 'http://test.host/help/integration/jira_development_panel#gitlabcom-1'
scopes: %w(READ WRITE DELETE),
apiVersion: 1,
apiMigrations: {
'context-qsh': true,
gdpr: true
}
)
expect(descriptor[:modules]).to include(
postInstallPage: {
key: 'gitlab-configuration',
name: { value: 'GitLab Configuration' },
url: '/subscriptions'
},
jiraDevelopmentTool: {
actions: {
createBranch: {
templateUrl: 'http://test.host/-/jira_connect/branches/new?issue_key={issue.key}&issue_summary={issue.summary}'
}
},
key: 'gitlab-development-tool',
application: { value: 'GitLab' },
name: { value: 'GitLab' },
url: 'https://gitlab.com',
logoUrl: logo_url,
capabilities: %w(branch commit pull_request)
},
jiraBuildInfoProvider: common_module_properties.merge(
actions: {},
name: { value: 'GitLab CI' },
key: 'gitlab-ci'
),
jiraDeploymentInfoProvider: common_module_properties.merge(
actions: {},
name: { value: 'GitLab Deployments' },
key: 'gitlab-deployments'
),
jiraFeatureFlagInfoProvider: common_module_properties.merge(
actions: {},
name: { value: 'GitLab Feature Flags' },
key: 'gitlab-feature-flags'
)
)
end
context 'when the jira_connect_create_branch feature is disabled' do
before do
stub_feature_flags(jira_connect_create_branch: false)
end
it 'does not include the create branch action' do
get :show
expect(response).to have_gitlab_http_status(:ok)
expect(descriptor[:modules][:jiraDevelopmentTool][:actions]).not_to include(:createBranch)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JiraConnect::BranchesController do
describe '#new' do
context 'when logged in' do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
it 'assigns the suggested branch name' do
get :new, params: { issue_key: 'ACME-123', issue_summary: 'My Issue !@#$%' }
expect(response).to be_successful
expect(assigns(:branch_name)).to eq('ACME-123-my-issue')
end
it 'ignores missing summary' do
get :new, params: { issue_key: 'ACME-123' }
expect(response).to be_successful
expect(assigns(:branch_name)).to eq('ACME-123')
end
it 'does not set a branch name if key is not passed' do
get :new, params: { issue_summary: 'My issue' }
expect(response).to be_successful
expect(assigns(:branch_name)).to be_nil
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(jira_connect_create_branch: false)
end
it 'renders a 404 error' do
get :new
expect(response).to be_not_found
end
end
end
context 'when not logged in' do
it 'redirects to the login page' do
get :new
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create GitLab branches from Jira', :js do
let_it_be(:alice) { create(:user, name: 'Alice') }
let_it_be(:bob) { create(:user, name: 'Bob') }
let_it_be(:project1) { create(:project, :repository, namespace: alice.namespace, title: 'foo') }
let_it_be(:project2) { create(:project, :repository, namespace: alice.namespace, title: 'bar') }
let_it_be(:project3) { create(:project, namespace: bob.namespace) }
let(:source_branch) { 'my-source-branch' }
let(:new_branch) { 'my-new-branch' }
before do
project2.repository.add_branch(alice, source_branch, 'master')
sign_in(alice)
end
def within_dropdown(&block)
within('.dropdown-menu', &block)
end
it 'select project and branch and submit the form' do
visit new_jira_connect_branch_path(issue_key: 'ACME-123', issue_summary: 'My issue !@#$% title')
expect(page).to have_field('Branch name', with: 'ACME-123-my-issue-title')
expect(page).to have_button('Create branch', disabled: true)
# Select project1
click_on 'Select a project'
within_dropdown do
expect(page).to have_text('Alice / foo')
expect(page).to have_text('Alice / bar')
expect(page).not_to have_text('Bob /')
fill_in 'Search', with: 'foo'
expect(page).not_to have_text('Alice / bar')
click_on 'Alice / foo'
end
expect(page).to have_button('Create branch', disabled: false)
click_on 'master'
within_dropdown do
fill_in 'Search', with: source_branch
expect(page).not_to have_text(source_branch)
fill_in 'Search', with: 'master'
expect(page).to have_text('master')
end
# Switch to project2
click_on 'Alice / foo'
within_dropdown do
fill_in 'Search', with: ''
click_on 'Alice / bar'
end
click_on 'master'
within_dropdown do
fill_in 'Search', with: source_branch
click_on source_branch
end
fill_in 'Branch name', with: new_branch
click_on 'Create branch'
expect(page).to have_text('New branch was successfully created. You can now close this window and return to Jira.')
expect(project1.commit(new_branch)).to be_nil
expect(project2.commit(new_branch)).not_to be_nil
expect(project2.commit(new_branch)).to eq(project2.commit(source_branch))
end
end
......@@ -614,33 +614,40 @@ RSpec.describe Issue do
let(:subject) { create :issue }
end
describe "#to_branch_name" do
let_it_be(:issue) { create(:issue, project: reusable_project, title: 'testing-issue') }
describe '.to_branch_name' do
it 'parameterizes arguments and joins with dashes' do
expect(described_class.to_branch_name(123, 'foo bar', '!@#$%', 'f!o@o#b$a%r^')).to eq('123-foo-bar-f-o-o-b-a-r')
end
it 'starts with the issue iid' do
expect(issue.to_branch_name).to match(/\A#{issue.iid}-[A-Za-z\-]+\z/)
it 'preserves the case in the first argument' do
expect(described_class.to_branch_name('ACME-!@#$-123', 'FoO BaR')).to eq('ACME-123-foo-bar')
end
it "contains the issue title if not confidential" do
expect(issue.to_branch_name).to match(/testing-issue\z/)
it 'truncates branch name to at most 100 characters' do
expect(described_class.to_branch_name('a' * 101)).to eq('a' * 100)
end
it "does not contain the issue title if confidential" do
issue = create(:issue, project: reusable_project, title: 'testing-issue', confidential: true)
expect(issue.to_branch_name).to match(/confidential-issue\z/)
it 'truncates dangling parts of the branch name' do
branch_name = described_class.to_branch_name(
999,
'Lorem ipsum dolor sit amet consectetur adipiscing elit Mauris sit amet ipsum id lacus custom fringilla convallis'
)
# 100 characters would've got us "999-lorem...lacus-custom-fri".
expect(branch_name).to eq('999-lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-mauris-sit-amet-ipsum-id-lacus-custom')
end
end
context 'issue title longer than 100 characters' do
let_it_be(:issue) { create(:issue, project: reusable_project, iid: 999, title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit Mauris sit amet ipsum id lacus custom fringilla convallis') }
describe '#to_branch_name' do
let_it_be(:issue) { create(:issue, project: reusable_project, iid: 123, title: 'Testing Issue') }
it "truncates branch name to at most 100 characters" do
expect(issue.to_branch_name.length).to be <= 100
end
it 'returns a branch name with the issue title if not confidential' do
expect(issue.to_branch_name).to eq('123-testing-issue')
end
it "truncates dangling parts of the branch name" do
# 100 characters would've got us "999-lorem...lacus-custom-fri".
expect(issue.to_branch_name).to eq("999-lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-mauris-sit-amet-ipsum-id-lacus-custom")
end
it 'returns a generic branch name if confidential' do
issue.confidential = true
expect(issue.to_branch_name).to eq('123-confidential-issue')
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment