Commit e950acfb authored by Mark Chao's avatar Mark Chao Committed by Sean McGivern

Resolve "Support GitLab subgroups in Jira development panel"

parent 05542079
......@@ -528,17 +528,36 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
end
end
# EE-specific
scope path: '/-/jira', as: :jira do
scope path: '*namespace_id', namespace_id: Gitlab::PathRegex.full_namespace_route_regex do
resources :projects, path: '/', constraints: { id: Gitlab::PathRegex.project_route_regex }, only: :show
scope path: ':project_id', constraints: { project_id: Gitlab::PathRegex.project_route_regex }, module: :projects do
resources :commit, only: :show, constraints: { id: /\h{7,40}/ }
get 'tree/*id', to: 'tree#show', as: nil
end
end
# EE-specific
scope path: '/-/jira', as: :jira do
scope path: '*namespace_id/:project_id',
namespace_id: Gitlab::Jira::Dvcs::ENCODED_ROUTE_REGEX,
project_id: Gitlab::Jira::Dvcs::ENCODED_ROUTE_REGEX do
get '/', to: redirect { |params, req|
::Gitlab::Jira::Dvcs.restore_full_path(
namespace: params[:namespace_id],
project: params[:project_id]
)
}
get 'commit/:id', constraints: { id: /\h{7,40}/ }, to: redirect { |params, req|
project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(
namespace: params[:namespace_id],
project: params[:project_id]
)
"/#{project_full_path}/commit/#{params[:id]}"
}
get 'tree/*id', as: nil, to: redirect { |params, req|
project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(
namespace: params[:namespace_id],
project: params[:project_id]
)
"/#{project_full_path}/tree/#{params[:id]}"
}
end
end
......@@ -50,7 +50,7 @@ from the left navigation menu. Click `Link GitHub account` to start creating a n
Select GitHub Enterprise for the `Host` field.
For the `Team or User Account` field, enter the group name of a GitLab group that you have access to.
For the `Team or User Account` field, enter the group name of a GitLab group that you have access to. This must be a top-level group, but all its subgroups will be imported.
![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png)
......@@ -97,7 +97,6 @@ Click the links to see your GitLab repository data.
## Limitations
- This integration is currently not supported on GitLab instances under a [relative url][relative-url] (e.g. `http://example.com/gitlab`).
- Projects under nested groups are not supported
[existing-jira]: ../user/project/integrations/jira.md
[jira-development-panel]: https://confluence.atlassian.com/adminjiraserver070/integrating-with-development-tools-776637096.html#Integratingwithdevelopmenttools-Developmentpanelonissues
......
---
title: Support GitLab subgroups in Jira development panel
merge_request: 6290
author:
type: added
......@@ -4,14 +4,14 @@
module API
module Github
module Entities
class Namespace < Grape::Entity
expose :path, as: :login
end
class Repository < Grape::Entity
expose :id
expose :namespace, as: :owner, using: Namespace
expose :path, as: :name
expose :owner do |project, options|
{ login: project.root_namespace.path }
end
expose :name do |project, options|
::Gitlab::Jira::Dvcs.encode_project_name(project)
end
end
class BranchCommit < Grape::Entity
......
......@@ -28,8 +28,10 @@ module API
not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
end
def find_project_with_access(full_path)
project = find_project!(full_path)
def find_project_with_access(params)
project = find_project!(
::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys)
)
not_found! unless licensed_project?(project)
project
end
......@@ -79,7 +81,11 @@ module API
use :pagination
end
get ':namespace/repos', requirements: NAMESPACE_ENDPOINT_REQUIREMENTS do
projects = current_user.authorized_projects.select { |project| licensed_project?(project) }
namespace = Namespace.find_by_full_path(params[:namespace])
not_found!('Namespace') unless namespace
projects = current_user.authorized_projects.where(namespace_id: namespace.self_and_descendants).to_a
projects.select! { |project| licensed_project?(project) }
projects = ::Kaminari.paginate_array(projects)
present paginate(projects), with: ::API::Github::Entities::Repository
end
......@@ -124,9 +130,7 @@ module API
use :pagination
end
get ':namespace/:project/branches', requirements: PROJECT_ENDPOINT_REQUIREMENTS do
namespace = params[:namespace]
project = params[:project]
user_project = find_project_with_access("#{namespace}/#{project}")
user_project = find_project_with_access(params)
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
......@@ -137,9 +141,7 @@ module API
use :project_full_path
end
get ':namespace/:project/commits/:sha', requirements: PROJECT_ENDPOINT_REQUIREMENTS do
namespace = params[:namespace]
project = params[:project]
user_project = find_project_with_access("#{namespace}/#{project}")
user_project = find_project_with_access(params)
commit = user_project.commit(params[:sha])
......
module Gitlab
module Jira
module Dvcs
ENCODED_SLASH = '@'.freeze
SLASH = '/'.freeze
ENCODED_ROUTE_REGEX = /[a-zA-Z0-9_\-\.#{ENCODED_SLASH}]+/
def self.encode_slash(path)
path.gsub(SLASH, ENCODED_SLASH)
end
def self.decode_slash(path)
path.gsub(ENCODED_SLASH, SLASH)
end
# To present two types of projects stored by Jira,
# Type 1 are projects imported prior to nested group support,
# those project names are not full_path, so they are presented differently
# to maintain backwards compatibility.
# Type 2 are projects imported after nested group support,
# those project names are encoded full path
#
# @param [Project] project
def self.encode_project_name(project)
if project.namespace.has_parent?
encode_slash(project.full_path)
else
project.path
end
end
# To interpret two types of project names stored by Jira (see `encode_project_name`)
#
# @param [String] project
# Either an encoded full path, or just project name
# @param [String] namespace
def self.restore_full_path(namespace:, project:)
if project.include?(ENCODED_SLASH)
project.gsub(ENCODED_SLASH, SLASH)
else
"#{namespace}/#{project}"
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Jira::Dvcs do
describe '.encode_slash' do
it 'replaces slash character' do
expect(described_class.encode_slash('a/b/c')).to eq('a@b@c')
end
it 'ignores path without slash' do
expect(described_class.encode_slash('foo')).to eq('foo')
end
end
describe '.decode_slash' do
it 'replaces slash character' do
expect(described_class.decode_slash('a@b@c')).to eq('a/b/c')
end
it 'ignores path without slash' do
expect(described_class.decode_slash('foo')).to eq('foo')
end
end
describe '.encode_project_name' do
let(:group) { create(:group)}
let(:project) { create(:project, group: group)}
context 'root group' do
it 'returns project path' do
expect(described_class.encode_project_name(project)).to eq(project.path)
end
end
context 'nested group' do
let(:group) { create(:group, :nested)}
it 'returns encoded project full path' do
expect(described_class.encode_project_name(project)).to eq(described_class.encode_slash(project.full_path))
end
end
end
describe '.restore_full_path' do
context 'project name is an encoded full path' do
it 'returns decoded project path' do
expect(described_class.restore_full_path(namespace: 'group1', project: 'group1@group2@project1')).to eq('group1/group2/project1')
end
end
context 'project name is not an encoded full path' do
it 'assumes project belongs to root namespace and returns full project path based on passed in namespace' do
expect(described_class.restore_full_path(namespace: 'group1', project: 'project1')).to eq('group1/project1')
end
end
end
end
......@@ -117,40 +117,78 @@ describe API::V3::Github do
end
describe 'GET /users/:namespace/repos' do
context 'authenticated' do
let(:group) { create(:group) }
let!(:group_project) { create(:project, group: group) }
let(:group) { create(:group, name: 'foo') }
def expect_project_under_namespace(projects, namespace, user)
get v3_api("/users/#{namespace.path}/repos", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('entities/github/repositories', dir: 'ee')
projects.each do |project|
hash = json_response.find do |hash|
hash['name'] == ::Gitlab::Jira::Dvcs.encode_project_name(project)
end
raise "Project #{project.full_path} not present in response" if hash.nil?
expect(hash['owner']['login']).to eq(namespace.name)
end
expect(json_response.size).to eq(projects.size)
end
context 'group namespace' do
let(:project) { create(:project, group: group) }
before do
stub_licensed_features(jira_dev_panel_integration: true)
group.add_master(user)
end
it 'returns an array of projects with github format' do
get v3_api('/users/foo/repos', user)
it 'returns an array of projects belonging to group with github format' do
expect_project_under_namespace([project], group, user)
end
end
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(2)
context 'nested group namespace', :nested_groups do
let(:group) { create(:group, :nested) }
let!(:parent_group_project) { create(:project, group: group.parent, name: 'parent_group_project') }
let!(:child_group_project) { create(:project, group: group, name: 'child_group_project') }
expect(response).to match_response_schema('entities/github/repositories', dir: 'ee')
before do
stub_licensed_features(jira_dev_panel_integration: true)
group.parent.add_master(user)
end
it 'returns 200 when namespace path include a dot' do
group = create(:group, path: 'foo.bar')
it 'returns an array of projects belonging to group with github format' do
expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user)
end
end
get v3_api("/users/#{group.path}/repos", user)
context 'user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
expect(response).to have_gitlab_http_status(200)
before do
stub_licensed_features(jira_dev_panel_integration: true)
end
it 'returns an array of projects belonging to user namespace with github format' do
expect_project_under_namespace([project], user.namespace, user)
end
end
it 'returns valid project path as name' do
get v3_api('/users/foo/repos', user)
context 'namespace path includes a dot' do
let(:project) { create(:project, group: group) }
let(:group) { create(:group, name: 'foo.bar') }
project_names = json_response.map { |r| r['name'] }
before do
stub_licensed_features(jira_dev_panel_integration: true)
group.add_master(user)
end
expect(project_names).to include(project.path, group_project.path)
it 'returns an array of projects belonging to group with github format' do
expect_project_under_namespace([project], group, user)
end
end
......@@ -164,7 +202,7 @@ describe API::V3::Github do
it 'filters unlicensed namespace projects' do
silver_plan = create(:silver_plan)
licensed_project = create(:project, :empty_repo)
licensed_project = create(:project, :empty_repo, group: group)
licensed_project.add_reporter(user)
licensed_project.namespace.update!(plan_id: silver_plan.id)
......@@ -172,12 +210,17 @@ describe API::V3::Github do
stub_application_setting_on_object(project, should_check_namespace_plan: true)
stub_application_setting_on_object(licensed_project, should_check_namespace_plan: true)
get v3_api('/users/foo/repos', user)
expect_project_under_namespace([licensed_project], group, user)
end
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(licensed_project.id)
context 'namespace does not exist' do
it 'responds with not found status' do
stub_licensed_features(jira_dev_panel_integration: true)
get v3_api("/users/noo/repos", user)
expect(response).to have_gitlab_http_status(404)
end
end
end
......
require 'rails_helper'
describe 'Jira referenced paths', type: :request do
let(:group) { create(:group, name: 'group') }
let(:sub_group) { create(:group, name: 'subgroup', parent: group) }
let(:group_project) { create(:project, name: 'group_project', namespace: group) }
let(:sub_group_project) { create(:project, name: 'sub_group_project', namespace: sub_group) }
before do
login_as user
end
describe 'redirects to projects#show' do
context 'without nested group' do
let(:user) { group_project.creator }
it 'redirects to project' do
get('/-/jira/group/group_project')
expect(response).to redirect_to('/group/group_project')
end
end
context 'with nested group' do
let(:user) { sub_group_project.creator }
it 'redirects to project for root group' do
get('/-/jira/group/group@sub_group@sub_group_project')
expect(response).to redirect_to('/group/sub_group/sub_group_project')
end
it 'redirects to project for nested group' do
get('/-/jira/group@sub_group/group@sub_group@sub_group_project')
expect(response).to redirect_to('/group/sub_group/sub_group_project')
end
end
end
describe 'redirects to projects/commit#show' do
context 'without nested group' do
let(:user) { group_project.creator }
it 'redirects to commits' do
get('/-/jira/group/group_project/commit/1234567')
expect(response).to redirect_to('/group/group_project/commit/1234567')
end
end
context 'with nested group' do
let(:user) { sub_group_project.creator }
it 'redirects to commits' do
get('/-/jira/group/group@sub_group@sub_group_project/commit/1234567')
expect(response).to redirect_to('/group/sub_group/sub_group_project/commit/1234567')
end
end
end
describe 'redirects to projects/tree#show' do
context 'without nested group' do
let(:user) { group_project.creator }
it 'redirects to tree' do
get('/-/jira/group/group_project/tree/1234567')
expect(response).to redirect_to('/group/group_project/tree/1234567')
end
end
context 'with nested group' do
let(:user) { sub_group_project.creator }
it 'redirects to tree' do
get('/-/jira/group/group@sub_group@sub_group_project/tree/1234567')
expect(response).to redirect_to('/group/sub_group/sub_group_project/tree/1234567')
end
end
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