Merge branch '33097-issue-tracker' into 'master'

Associate Issues tab only with internal issues tracker

Closes #33097

See merge request !12130
This commit is contained in:
Sean McGivern 2017-07-24 13:00:33 +00:00
commit 6d13bc5863
38 changed files with 655 additions and 129 deletions

View File

@ -8,7 +8,6 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new]
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
@ -243,19 +242,19 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
return render_404 unless can?(current_user, :admin_issue, @project)
render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request!
return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
return render_404 unless @project.feature_available?(:issues, current_user)
end
def redirect_to_external_issue_tracker

View File

@ -17,10 +17,10 @@ module IssuesHelper
return '' if project.nil?
url =
if options[:only_path]
project.issues_tracker.issue_path(issue_iid)
if options[:internal]
url_for_internal_issue(issue_iid, project, options)
else
project.issues_tracker.issue_url(issue_iid)
url_for_tracker_issue(issue_iid, project, options)
end
# Ensure we return a valid URL to prevent possible XSS.
@ -29,6 +29,24 @@ module IssuesHelper
''
end
def url_for_tracker_issue(issue_iid, project, options)
if options[:only_path]
project.issues_tracker.issue_path(issue_iid)
else
project.issues_tracker.issue_url(issue_iid)
end
end
def url_for_internal_issue(issue_iid, project = @project, options = {})
helpers = Gitlab::Routing.url_helpers
if options[:only_path]
helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid)
else
helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid)
end
end
def bulk_update_milestone_options
milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
@ -158,4 +176,6 @@ module IssuesHelper
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
module_function :url_for_tracker_issue
end

View File

@ -596,7 +596,7 @@ class MergeRequest < ActiveRecord::Base
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker?
return unless project.issues_enabled?
transaction do
self.merge_requests_closing_issues.delete_all

View File

@ -734,9 +734,11 @@ class Project < ActiveRecord::Base
end
def get_issue(issue_id, current_user)
if default_issues_tracker?
IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
else
issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?
if issue
issue
elsif external_issue_tracker
ExternalIssue.new(issue_id, self)
end
end
@ -758,7 +760,7 @@ class Project < ActiveRecord::Base
end
def external_issue_reference_pattern
external_issue_tracker.class.reference_pattern
external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
end
def default_issues_tracker?

View File

@ -8,8 +8,12 @@ class IssueTrackerService < Service
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern
@reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
def self.reference_pattern(only_long: false)
if only_long
%r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)}
else
%r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end
end
def default?

View File

@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern
def self.reference_pattern(only_long: true)
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end

View File

@ -287,9 +287,6 @@ class ProjectPolicy < BasePolicy
prevent :create_issue
prevent :update_issue
prevent :admin_issue
end
rule { issues_disabled & default_issues_tracker }.policy do
prevent :read_issue
end

View File

@ -16,13 +16,13 @@ module Issues
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
project.jira_service.close_issue(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
if project.default_issues_tracker? && issue.close
if project.issues_enabled? && issue.close
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications

View File

@ -75,10 +75,10 @@
Registry
- if project_nav_tab? :issues
= nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
- if @project.default_issues_tracker?
- if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
Issues
@ -113,7 +113,7 @@
Milestones
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)

View File

@ -23,16 +23,16 @@
Registry
- if project_nav_tab? :issues
= nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
- if @project.default_issues_tracker?
- if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker?
- controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled?
= nav_link(controller: controllers) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span

View File

@ -4,7 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
- unless @project.default_issues_tracker?
- unless @project.issues_enabled?
= content_for :sub_nav do
= render "projects/merge_requests/head"

View File

@ -1,4 +1,4 @@
- if @project.default_issues_tracker?
- if @project.issues_enabled?
= render "projects/issues/head"
- else
= render "projects/merge_requests/head"

View File

@ -0,0 +1,4 @@
---
title: Associate Issues tab only with internal issues tracker
merge_request:
author:

View File

@ -4,14 +4,12 @@ GitLab has a great issue tracker but you can also use an external one such as
Jira, Redmine, or Bugzilla. Issue trackers are configurable per GitLab project and allow
you to do the following:
- the **Issues** link on the GitLab project pages takes you to the appropriate
issue index of the external tracker
- clicking **New issue** on the project dashboard creates a new issue on the
external tracker
- you can reference these external issues inside GitLab interface
(merge requests, commits, comments) and they will be automatically converted
into links
You can have enabled both external and internal GitLab issue trackers in parallel. The **Issues** link always opens the internal issue tracker and in case the internal issue tracker is disabled the link is not visible in the menu.
## Configuration
The configuration is done via a project's **Services**.

View File

@ -20,10 +20,12 @@ Once you have configured and enabled Bugzilla:
## Referencing issues in Bugzilla
Issues in Bugzilla can be referenced in two alternative ways:
1. `#<ID>` where `<ID>` is a number (example `#143`)
1. `#<ID>` where `<ID>` is a number (example `#143`).
2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
We suggest using the longer format if you have both internal and external issue trackers enabled. If you use the shorter format and an issue with the same ID exists in the internal issue tracker the internal issue will be linked.
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.

View File

@ -30,5 +30,7 @@ Issues in Redmine can be referenced in two alternative ways:
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
We suggest using the longer format if you have both internal and external issue trackers enabled. If you use the shorter format and an issue with the same ID exists in the internal issue tracker the internal issue will be linked.
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.

View File

@ -109,7 +109,7 @@ module API
user.avatar_url(only_path: false)
end
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs
expose :ci_config_path

View File

@ -29,14 +29,6 @@ module API
render_api_error!(errors, 400)
end
def issue_entity(project)
if project.has_external_issue_tracker?
Entities::ExternalIssue
else
Entities::IssueBasic
end
end
def find_merge_requests(args = {})
args = params.merge(args)
@ -278,7 +270,14 @@ module API
get ':id/merge_requests/:merge_request_iid/closes_issues' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
issues = paginate(issues)
external_issues, internal_issues = issues.partition { |issue| issue.is_a?(ExternalIssue) }
data = Entities::IssueBasic.represent(internal_issues, current_user: current_user)
data += Entities::ExternalIssue.represent(external_issues, current_user: current_user)
data.as_json
end
end
end

View File

@ -20,7 +20,7 @@ module Banzai
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true)
end
def project_from_ref(ref)

View File

@ -21,10 +21,14 @@ module Banzai
gather_attributes_per_project(nodes, self.class.data_attribute)
end
private
# we extract only external issue trackers references here, we don't extract cross-project references,
# so we don't need to do anything here.
def can_read_reference?(user, ref_project, node)
can?(user, :read_issue, ref_project)
true
end
def nodes_visible_to_user(user, nodes)
nodes
end
end
end

View File

@ -33,7 +33,12 @@ module Gitlab
def issues
if project && project.jira_tracker?
@references[:external_issue] ||= references(:external_issue)
if project.issues_enabled?
@references[:all_issues] ||= references(:external_issue) + references(:issue)
else
@references[:external_issue] ||= references(:external_issue) +
references(:issue).select { |i| i.project_id != project.id }
end
else
@references[:issue] ||= references(:issue)
end

View File

@ -2,7 +2,7 @@ module Gitlab
module SlashCommands
class IssueCommand < BaseCommand
def self.available?(project)
project.issues_enabled? && project.default_issues_tracker?
project.issues_enabled?
end
def collection

View File

@ -7,16 +7,30 @@ describe Projects::IssuesController do
describe "GET #index" do
context 'external issue tracker' do
let!(:service) do
create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker', project_url: 'http://test.com')
before do
sign_in(user)
project.add_developer(user)
create(:jira_service, project: project)
end
it 'redirects to the external issue tracker' do
controller.instance_variable_set(:@project, project)
context 'when GitLab issues disabled' do
it 'returns 404 status' do
project.issues_enabled = false
project.save!
get :index, namespace_id: project.namespace, project_id: project
get :index, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to(service.issue_tracker_path)
expect(response).to have_http_status(404)
end
end
context 'when GitLab issues enabled' do
it 'renders the "index" template' do
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
end
end
end
@ -42,15 +56,7 @@ describe Projects::IssuesController do
it "returns 404 when issues are disabled" do
project.issues_enabled = false
project.save
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
end
it "returns 404 when external issue tracker is enabled" do
controller.instance_variable_set(:@project, project)
allow(project).to receive(:default_issues_tracker?).and_return(false)
project.save!
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
@ -148,14 +154,29 @@ describe Projects::IssuesController do
before do
sign_in(user)
project.team << [user, :developer]
external = double
allow(project).to receive(:external_issue_tracker).and_return(external)
end
it 'redirects to the external issue tracker' do
controller.instance_variable_set(:@project, project)
context 'when GitLab issues disabled' do
it 'returns 404 status' do
project.issues_enabled = false
project.save!
get :new, namespace_id: project.namespace, project_id: project
get :new, namespace_id: project.namespace, project_id: project
expect(response).to redirect_to('http://test.com')
expect(response).to have_http_status(404)
end
end
context 'when GitLab issues enabled' do
it 'renders the "new" template' do
get :new, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:new)
end
end
end
end

View File

@ -0,0 +1,193 @@
require 'rails_helper'
describe 'Markdown References', :feature, :js do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project)}
let(:issue_actual_project) { create(:issue, project: actual_project) }
let!(:other_project) { create(:empty_project, :public) }
let!(:issue_other_project) { create(:issue, project: other_project) }
let(:issues) { [issue_actual_project, issue_other_project] }
def build_note
markdown = "Referencing internal issue #{issue_actual_project.to_reference}, " +
"cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5 " +
"and non existing #999"
page.within('#diff-notes-app') do
fill_in 'note_note', with: markdown
end
end
shared_examples 'correct references' do
before do
remotelink = double(:remotelink, all: [], build: double(save!: true))
stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
sign_in(user)
visit merge_request_path(merge_request)
build_note
end
def links_expectations
issues.each do |issue|
if referenced_issues.include?(issue)
expect(page).to have_link(issue.to_reference, href: issue_path(issue))
else
expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
end
end
if jira_referenced
expect(page).to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
else
expect(page).not_to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
end
expect(page).not_to have_link('#999')
end
it 'creates a link to the referenced issue on the preview' do
find('.js-md-preview-button').click
wait_for_requests
page.within('.md-preview-holder') do
links_expectations
end
end
it 'creates a link to the referenced issue after submit' do
click_button 'Comment'
wait_for_requests
page.within('#diff-notes-app') do
links_expectations
end
end
it 'creates a note on the referenced issues' do
click_button 'Comment'
wait_for_requests
if referenced_issues.include?(issue_actual_project)
visit issue_path(issue_actual_project)
page.within('#notes') do
expect(page).to have_content(
"#{user.to_reference} mentioned in merge request #{merge_request.to_reference}"
)
end
end
if referenced_issues.include?(issue_other_project)
visit issue_path(issue_other_project)
page.within('#notes') do
expect(page).to have_content(
"#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}"
)
end
end
end
end
context 'when internal issues tracker is enabled for the other project' do
context 'when only internal issues tracker is enabled for the actual project' do
include_examples 'correct references' do
let(:referenced_issues) { [issue_actual_project, issue_other_project] }
let(:jira_referenced) { false }
end
end
context 'when both external and internal issues trackers are enabled for the actual project' do
before do
create(:jira_service, project: actual_project)
end
include_examples 'correct references' do
let(:referenced_issues) { [issue_actual_project, issue_other_project] }
let(:jira_referenced) { true }
end
end
context 'when only external issues tracker is enabled for the actual project' do
before do
create(:jira_service, project: actual_project)
actual_project.issues_enabled = false
actual_project.save!
end
include_examples 'correct references' do
let(:referenced_issues) { [issue_other_project] }
let(:jira_referenced) { true }
end
end
context 'when no tracker is enabled for the actual project' do
before do
actual_project.issues_enabled = false
actual_project.save!
end
include_examples 'correct references' do
let(:referenced_issues) { [issue_other_project] }
let(:jira_referenced) { false }
end
end
end
context 'when internal issues tracker is disabled for the other project' do
before do
other_project.issues_enabled = false
other_project.save!
end
context 'when only internal issues tracker is enabled for the actual project' do
include_examples 'correct references' do
let(:referenced_issues) { [issue_actual_project] }
let(:jira_referenced) { false }
end
end
context 'when both external and internal issues trackers are enabled for the actual project' do
before do
create(:jira_service, project: actual_project)
end
include_examples 'correct references' do
let(:referenced_issues) { [issue_actual_project] }
let(:jira_referenced) { true }
end
end
context 'when only external issues tracker is enabled for the actual project' do
before do
create(:jira_service, project: actual_project)
actual_project.issues_enabled = false
actual_project.save!
end
include_examples 'correct references' do
let(:referenced_issues) { [] }
let(:jira_referenced) { true }
end
end
context 'when no issues tracker is enabled for the actual project' do
before do
actual_project.issues_enabled = false
actual_project.save!
end
include_examples 'correct references' do
let(:referenced_issues) { [] }
let(:jira_referenced) { false }
end
end
end
end

View File

@ -39,14 +39,25 @@ describe 'Edit Project Settings', feature: true do
end
end
context "When external issue tracker is enabled" do
it "does not hide issues tab" do
project.project_feature.update(issues_access_level: ProjectFeature::DISABLED)
context 'When external issue tracker is enabled and issues enabled on project settings' do
it 'does not hide issues tab' do
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
visit project_path(project)
expect(page).to have_selector(".shortcuts-issues")
expect(page).to have_selector('.shortcuts-issues')
end
end
context 'When external issue tracker is enabled and issues disabled on project settings' do
it 'hides issues tab' do
project.issues_enabled = false
project.save!
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
visit project_path(project)
expect(page).not_to have_selector('.shortcuts-issues')
end
end

View File

@ -8,7 +8,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
let(:int_expected) { polymorphic_path([@project.namespace, @project, issue]) }
it "returns internal path if used internal tracker" do
@project = project
@ -22,6 +22,12 @@ describe IssuesHelper do
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
it "returns path to internal issue when internal option passed" do
@project = ext_project
expect(url_for_issue(issue.iid, ext_project, internal: true)).to match(int_expected)
end
it "returns empty string if project nil" do
@project = nil

View File

@ -108,6 +108,11 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
let(:issue) { ExternalIssue.new("#123", project) }
let(:reference) { issue.to_reference }
before do
project.issues_enabled = false
project.save!
end
it_behaves_like "external issue tracker"
end

View File

@ -4,26 +4,87 @@ describe Banzai::Pipeline::GfmPipeline do
describe 'integration between parsing regular and external issue references' do
let(:project) { create(:redmine_project, :public) }
it 'allows to use shorthand external reference syntax for Redmine' do
markdown = '#12'
context 'when internal issue tracker is enabled' do
context 'when shorthand pattern #ISSUE_ID is used' do
it 'links an internal issue if it exists' do
issue = create(:issue, project: project)
markdown = issue.to_reference(project, full: true)
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
expect(link['href']).to eq(
Gitlab::Routing.url_helpers.project_issue_path(project, issue)
)
end
it 'does not link any issue if it does not exist on GitLab' do
markdown = '#12'
result = described_class.call(markdown, project: project)[:output]
expect(result.css('a')).to be_empty
end
end
it 'allows to use long external reference syntax for Redmine' do
markdown = 'API_32-12'
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
end
it 'parses cross-project references to regular issues' do
other_project = create(:empty_project, :public)
issue = create(:issue, project: other_project)
markdown = issue.to_reference(project, full: true)
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq(
Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
)
end
end
it 'parses cross-project references to regular issues' do
other_project = create(:empty_project, :public)
issue = create(:issue, project: other_project)
markdown = issue.to_reference(project, full: true)
context 'when internal issue tracker is disabled' do
before do
project.issues_enabled = false
project.save!
end
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
it 'allows to use shorthand external reference syntax for Redmine' do
markdown = '#12'
expect(link['href']).to eq(
Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
)
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
end
it 'allows to use long external reference syntax for Redmine' do
markdown = 'API_32-12'
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
end
it 'parses cross-project references to regular issues' do
other_project = create(:empty_project, :public)
issue = create(:issue, project: other_project)
markdown = issue.to_reference(project, full: true)
result = described_class.call(markdown, project: project)[:output]
link = result.css('a').first
expect(link['href']).to eq(
Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
)
end
end
end
end

View File

@ -183,11 +183,34 @@ describe Gitlab::ReferenceExtractor, lib: true do
context 'with an external issue tracker' do
let(:project) { create(:jira_project) }
let(:issue) { create(:issue, project: project) }
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('FOOBAR-4567', project)]
context 'when GitLab issues are enabled' do
it 'returns both JIRA and internal issues' do
subject.analyze("JIRA-123 and FOOBAR-4567 and #{issue.to_reference}")
expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('FOOBAR-4567', project),
issue]
end
it 'returns only JIRA issues if the internal one does not exists' do
subject.analyze("JIRA-123 and FOOBAR-4567 and #999")
expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('FOOBAR-4567', project)]
end
end
context 'when GitLab issues are disabled' do
before do
project.issues_enabled = false
project.save!
end
it 'returns only JIRA issues' do
subject.analyze("JIRA-123 and FOOBAR-4567 and #{issue.to_reference}")
expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
ExternalIssue.new('FOOBAR-4567', project)]
end
end
end

View File

@ -174,25 +174,25 @@ describe Commit, 'Mentionable' do
it "is false when message doesn't reference anything" do
allow(commit.raw).to receive(:message).and_return "WIP: Do something"
expect(commit.matches_cross_reference_regex?).to be false
expect(commit.matches_cross_reference_regex?).to be_falsey
end
it 'is true if issue #number mentioned in title' do
allow(commit.raw).to receive(:message).and_return "#1"
expect(commit.matches_cross_reference_regex?).to be true
expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if references an MR' do
allow(commit.raw).to receive(:message).and_return "See merge request !12"
expect(commit.matches_cross_reference_regex?).to be true
expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if references a commit' do
allow(commit.raw).to receive(:message).and_return "a1b2c3d4"
expect(commit.matches_cross_reference_regex?).to be true
expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if issue referenced by url' do
@ -200,7 +200,7 @@ describe Commit, 'Mentionable' do
allow(commit.raw).to receive(:message).and_return Gitlab::UrlBuilder.build(issue)
expect(commit.matches_cross_reference_regex?).to be true
expect(commit.matches_cross_reference_regex?).to be_truthy
end
context 'with external issue tracker' do
@ -209,7 +209,13 @@ describe Commit, 'Mentionable' do
it 'is true if external issues referenced' do
allow(commit.raw).to receive(:message).and_return 'JIRA-123'
expect(commit.matches_cross_reference_regex?).to be true
expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if internal issues referenced' do
allow(commit.raw).to receive(:message).and_return '#123'
expect(commit.matches_cross_reference_regex?).to be_truthy
end
end
end

View File

@ -155,13 +155,53 @@ describe MergeRequest, models: true do
expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
subject.project.update_attribute(:has_external_issue_tracker, true)
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
context 'when both internal and external issue trackers are enabled' do
before do
subject.project.has_external_issue_tracker = true
subject.project.save!
end
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
it 'caches an internal issue' do
issue = create(:issue, project: subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }
.to change(subject.merge_requests_closing_issues, :count).by(1)
end
end
context 'when only external issue tracker enabled' do
before do
subject.project.has_external_issue_tracker = true
subject.project.issues_enabled = false
subject.project.save!
end
it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
it 'does not cache an internal issue' do
issue = create(:issue, project: subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }
.not_to change(subject.merge_requests_closing_issues, :count)
end
end
end

View File

@ -533,15 +533,48 @@ describe Project, models: true do
end
context 'with external issues tracker' do
let!(:internal_issue) { create(:issue, project: project) }
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
allow(project).to receive(:external_issue_tracker).and_return(true)
end
it 'returns an ExternalIssue' do
issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq 'FOO-1234'
expect(issue.project).to eq project
context 'when internal issues are enabled' do
it 'returns interlan issue' do
issue = project.get_issue(internal_issue.iid, user)
expect(issue).to be_kind_of(Issue)
expect(issue.iid).to eq(internal_issue.iid)
expect(issue.project).to eq(project)
end
it 'returns an ExternalIssue when internal issue does not exists' do
issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq('FOO-1234')
expect(issue.project).to eq(project)
end
end
context 'when internal issues are disabled' do
before do
project.issues_enabled = false
project.save!
end
it 'returns always an External issues' do
issue = project.get_issue(internal_issue.iid, user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq(internal_issue.iid.to_s)
expect(issue.project).to eq(project)
end
it 'returns an ExternalIssue when internal issue does not exists' do
issue = project.get_issue('FOO-1234', user)
expect(issue).to be_kind_of(ExternalIssue)
expect(issue.iid).to eq('FOO-1234')
expect(issue.project).to eq(project)
end
end
end
end

View File

@ -103,6 +103,30 @@ describe ProjectPolicy, models: true do
end
end
context 'issues feature' do
subject { described_class.new(owner, project) }
context 'when the feature is disabled' do
it 'does not include the issues permissions' do
project.issues_enabled = false
project.save!
expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue
end
end
context 'when the feature is disabled and external tracker configured' do
it 'does not include the issues permissions' do
create(:jira_service, project: project)
project.issues_enabled = false
project.save!
expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue
end
end
end
context 'abilities for non-public projects' do
let(:project) { create(:empty_project, namespace: owner.namespace) }

View File

@ -794,18 +794,24 @@ describe API::MergeRequests do
it 'handles external issues' do
jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
ext_issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
issue = create(:issue, project: jira_project)
description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}"
merge_request = create(:merge_request,
:simple, author: user, assignee: user, source_project: jira_project, description: description)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.length).to eq(2)
expect(json_response.second['title']).to eq(ext_issue.title)
expect(json_response.second['id']).to eq(ext_issue.id)
expect(json_response.second['confidential']).to be_nil
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.first['confidential']).not_to be_nil
end
it 'returns 403 if the user has no access to the merge request' do

View File

@ -159,6 +159,31 @@ describe API::Projects do
expect(json_response.first).to include 'statistics'
end
context 'when external issue tracker is enabled' do
let!(:jira_service) { create(:jira_service, project: project) }
it 'includes open_issues_count' do
get api('/projects', user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
expect(json_response.find { |hash| hash['id'] == project.id }.keys).to include('open_issues_count')
end
it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects', user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end
end
context 'and with simple=true' do
it 'returns a simplified version of all the projects' do
expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)

View File

@ -488,21 +488,57 @@ describe GitPushService, services: true do
end
end
context "using wrong markdown" do
let(:message) { "this is some work.\n\ncloses #1" }
context "using internal issue reference" do
context 'when internal issues are disabled' do
before do
project.issues_enabled = false
project.save!
end
let(:message) { "this is some work.\n\ncloses #1" }
it "does not initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
it "does not initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
end
it "does not initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
end
it "does not initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
context 'when internal issues are enabled' do
let(:issue) { create(:issue, project: project) }
let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" }
expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
it "initiates one api call to jira server to close the jira issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the jira issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
it "closes the internal issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
expect(issue.reload).to be_closed
end
it "adds a note indicating that the issue is now closed" do
expect(SystemNoteService).to receive(:change_status)
.with(issue, project, commit_author, "closed", closing_commit)
execute_service(project, commit_author, @oldrev, @newrev, @ref )
end
end
end
end

View File

@ -98,13 +98,13 @@ describe Issues::CloseService, services: true do
end
end
context 'external issue tracker' do
context 'internal issues disabled' do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
described_class.new(project, user).close_issue(issue)
project.issues_enabled = false
project.save!
end
it 'closes the issue' do
it 'does not close the issue' do
expect(issue).to be_valid
expect(issue).to be_opened
expect(todo.reload).to be_pending

View File

@ -207,7 +207,7 @@ describe MergeRequests::BuildService, services: true do
let(:source_branch) { '12345-fix-issue' }
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
allow(project).to receive(:external_issue_tracker).and_return(true)
end
it 'sets the title to: Resolves External Issue $issue-iid' do