Commit 0ca6bf4d authored by Sean McGivern's avatar Sean McGivern

Merge branch 'adam-external-issue-references-spike' into 'master'

Improve support for external issue references

Closes #33679, #34076, and #34082

See merge request !12485
parents 770bed58 9da30769
...@@ -14,7 +14,7 @@ module Mentionable ...@@ -14,7 +14,7 @@ module Mentionable
end end
EXTERNAL_PATTERN = begin EXTERNAL_PATTERN = begin
issue_pattern = ExternalIssue.reference_pattern issue_pattern = IssueTrackerService.reference_pattern
link_patterns = URI.regexp(%w(http https)) link_patterns = URI.regexp(%w(http https))
reference_pattern(link_patterns, issue_pattern) reference_pattern(link_patterns, issue_pattern)
end end
......
...@@ -38,11 +38,6 @@ class ExternalIssue ...@@ -38,11 +38,6 @@ class ExternalIssue
@project.id @project.id
end end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil, full: nil) def to_reference(_from_project = nil, full: nil)
id id
end end
......
...@@ -727,8 +727,8 @@ class Project < ActiveRecord::Base ...@@ -727,8 +727,8 @@ class Project < ActiveRecord::Base
end end
end end
def issue_reference_pattern def external_issue_reference_pattern
issues_tracker.reference_pattern external_issue_tracker.class.reference_pattern
end end
def default_issues_tracker? def default_issues_tracker?
......
...@@ -5,7 +5,10 @@ class IssueTrackerService < Service ...@@ -5,7 +5,10 @@ class IssueTrackerService < Service
# Pattern used to extract links from comments # Pattern used to extract links from comments
# Override this method on services that uses different patterns # Override this method on services that uses different patterns
def reference_pattern # 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+)} @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end end
......
...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService ...@@ -18,7 +18,7 @@ class JiraService < IssueTrackerService
end end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end end
......
---
title: Improve support for external issue references
merge_request: 12485
author:
...@@ -8,6 +8,9 @@ you to do the following: ...@@ -8,6 +8,9 @@ you to do the following:
issue index of the external tracker issue index of the external tracker
- clicking **New issue** on the project dashboard creates a new issue on the - clicking **New issue** on the project dashboard creates a new issue on the
external tracker external tracker
- you can reference these external issues inside GitLab interface
(merge requests, commits, comments) and they will be automatically converted
into links
## Configuration ## Configuration
......
...@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla: ...@@ -16,3 +16,14 @@ Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate - the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page Bugzilla product page
- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue - clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
## Referencing issues in Bugzilla
Issues in Bugzilla can be referenced in two alternative ways:
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`).
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
...@@ -21,3 +21,14 @@ Once you have configured and enabled Redmine: ...@@ -21,3 +21,14 @@ Once you have configured and enabled Redmine:
As an example, below is a configuration for a project named gitlab-ci. As an example, below is a configuration for a project named gitlab-ci.
![Redmine configuration](img/redmine_configuration.png) ![Redmine configuration](img/redmine_configuration.png)
## Referencing issues in Redmine
Issues in Redmine can be referenced in two alternative ways:
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`).
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
...@@ -216,12 +216,7 @@ module Banzai ...@@ -216,12 +216,7 @@ module Banzai
@references_per_project ||= begin @references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
if uses_reference_pattern?
Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
else
object_class.link_reference_pattern
end
nodes.each do |node| nodes.each do |node|
node.to_html.scan(regex) do node.to_html.scan(regex) do
...@@ -323,14 +318,6 @@ module Banzai ...@@ -323,14 +318,6 @@ module Banzai
value value
end end
end end
# There might be special cases like filters
# that should ignore reference pattern
# eg: IssueReferenceFilter when using a external issues tracker
# In those cases this method should be overridden on the filter subclass
def uses_reference_pattern?
true
end
end end
end end
end end
...@@ -3,6 +3,8 @@ module Banzai ...@@ -3,6 +3,8 @@ module Banzai
# HTML filter that replaces external issue tracker references with links. # HTML filter that replaces external issue tracker references with links.
# References are ignored if the project doesn't use an external issue # References are ignored if the project doesn't use an external issue
# tracker. # tracker.
#
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue self.reference_type = :external_issue
...@@ -87,7 +89,7 @@ module Banzai ...@@ -87,7 +89,7 @@ module Banzai
end end
def issue_reference_pattern def issue_reference_pattern
external_issues_cached(:issue_reference_pattern) external_issues_cached(:external_issue_reference_pattern)
end end
private private
......
...@@ -15,10 +15,6 @@ module Banzai ...@@ -15,10 +15,6 @@ module Banzai
Issue Issue
end end
def uses_reference_pattern?
context[:project].default_issues_tracker?
end
def find_object(project, iid) def find_object(project, iid)
issues_per_project[project][iid] issues_per_project[project][iid]
end end
...@@ -38,13 +34,7 @@ module Banzai ...@@ -38,13 +34,7 @@ module Banzai
projects_per_reference.each do |path, project| projects_per_reference.each do |path, project|
issue_ids = references_per_project[path] issue_ids = references_per_project[path]
issues = project.issues.where(iid: issue_ids.to_a)
issues =
if project.default_issues_tracker?
project.issues.where(iid: issue_ids.to_a)
else
issue_ids.map { |id| ExternalIssue.new(id, project) }
end
issues.each do |issue| issues.each do |issue|
hash[project][issue.iid.to_i] = issue hash[project][issue.iid.to_i] = issue
...@@ -55,26 +45,6 @@ module Banzai ...@@ -55,26 +45,6 @@ module Banzai
end end
end end
def object_link_title(object)
if object.is_a?(ExternalIssue)
"Issue in #{object.project.external_issue_tracker.title}"
else
super
end
end
def data_attributes_for(text, project, object, link: false)
if object.is_a?(ExternalIssue)
data_attribute(
project: project.id,
external_issue: object.id,
reference_type: ExternalIssueReferenceFilter.reference_type
)
else
super
end
end
def projects_relation_for_paths(paths) def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service) super(paths).includes(:gitlab_issue_tracker_service)
end end
......
...@@ -4,9 +4,6 @@ module Banzai ...@@ -4,9 +4,6 @@ module Banzai
self.reference_type = :issue self.reference_type = :issue
def nodes_visible_to_user(user, nodes) def nodes_visible_to_user(user, nodes)
# It is not possible to check access rights for external issue trackers
return nodes if project && project.external_issue_tracker
issues = issues_for_nodes(nodes) issues = issues_for_nodes(nodes)
readable_issues = Ability readable_issues = Ability
......
...@@ -220,7 +220,7 @@ FactoryGirl.define do ...@@ -220,7 +220,7 @@ FactoryGirl.define do
active: true, active: true,
properties: { properties: {
'project_url' => 'http://redmine/projects/project_name_in_redmine', 'project_url' => 'http://redmine/projects/project_name_in_redmine',
'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", 'issues_url' => 'http://redmine/projects/project_name_in_redmine/issues/:id',
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
} }
) )
......
...@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do ...@@ -88,12 +88,12 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'queries the collection on the first call' do it 'queries the collection on the first call' do
expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original
not_cached = reference_filter.call("look for #{reference}", { project: project }) not_cached = reference_filter.call("look for #{reference}", { project: project })
expect_any_instance_of(Project).not_to receive(:default_issues_tracker?) expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
expect_any_instance_of(Project).not_to receive(:issue_reference_pattern) expect_any_instance_of(Project).not_to receive(:external_issue_reference_pattern)
cached = reference_filter.call("look for #{reference}", { project: project }) cached = reference_filter.call("look for #{reference}", { project: project })
......
...@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -39,13 +39,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { "##{issue.iid}" } let(:reference) { "##{issue.iid}" }
it 'ignores valid references when using non-default tracker' do
allow(project).to receive(:default_issues_tracker?).and_return(false)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
end
it 'links to a valid reference' do it 'links to a valid reference' do
doc = reference_filter("Fixed #{reference}") doc = reference_filter("Fixed #{reference}")
...@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -340,24 +333,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
.to eq({ project => { issue.iid => issue } }) .to eq({ project => { issue.iid => issue } })
end end
end end
context 'using an external issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(project).to receive(:default_issues_tracker?).and_return(false)
expect(filter).to receive(:projects_per_reference)
.and_return({ project.path_with_namespace => project })
expect(filter).to receive(:references_per_project)
.and_return({ project.path_with_namespace => Set.new([1]) })
expect(filter.issues_per_project[project][1])
.to be_an_instance_of(ExternalIssue)
end
end
end end
describe '.references_in' do describe '.references_in' do
......
require 'rails_helper'
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'
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.namespace_project_issue_path(
other_project.namespace,
other_project,
issue
)
)
end
end
end
...@@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do ...@@ -39,16 +39,6 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
expect(subject.nodes_visible_to_user(user, [link])).to eq([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end end
end end
context 'when the project uses an external issue tracker' do
it 'returns all nodes' do
link = double(:link)
expect(project).to receive(:external_issue_tracker).and_return(true)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
end
end end
describe '#referenced_by' do describe '#referenced_by' do
......
...@@ -64,12 +64,12 @@ describe JiraService, models: true do ...@@ -64,12 +64,12 @@ describe JiraService, models: true do
end end
end end
describe '#reference_pattern' do describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern' it_behaves_like 'allows project key on reference pattern'
it 'does not allow # on the code' do it 'does not allow # on the code' do
expect(subject.reference_pattern.match('#123')).to be_nil expect(described_class.reference_pattern.match('#123')).to be_nil
expect(subject.reference_pattern.match('1#23#12')).to be_nil expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end end
end end
......
...@@ -31,11 +31,11 @@ describe RedmineService, models: true do ...@@ -31,11 +31,11 @@ describe RedmineService, models: true do
end end
end end
describe '#reference_pattern' do describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern' it_behaves_like 'allows project key on reference pattern'
it 'does allow # on the reference' do it 'does allow # on the reference' do
expect(subject.reference_pattern.match('#123')[:issue]).to eq('123') expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123')
end end
end end
end end
...@@ -401,18 +401,6 @@ describe GitPushService, services: true do ...@@ -401,18 +401,6 @@ describe GitPushService, services: true do
expect(SystemNoteService).not_to receive(:cross_reference) expect(SystemNoteService).not_to receive(:cross_reference)
execute_service(project, commit_author, @oldrev, @newrev, @ref ) execute_service(project, commit_author, @oldrev, @newrev, @ref )
end end
it "doesn't close issues when external issue tracker is in use" do
allow_any_instance_of(Project).to receive(:default_issues_tracker?)
.and_return(false)
external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
expect do
execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' )
end.not_to change { Note.where(project_id: project.id, system: true).count }
end
end end
context "to non-default branches" do context "to non-default branches" do
......
...@@ -8,15 +8,15 @@ end ...@@ -8,15 +8,15 @@ end
RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
it 'allows underscores in the project name' do it 'allows underscores in the project name' do
expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
end end
it 'allows numbers in the project name' do it 'allows numbers in the project name' do
expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
end end
it 'requires the project name to begin with A-Z' do it 'requires the project name to begin with A-Z' do
expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil
expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
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