diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 1f7db9b2eb8..d4a91e533c1 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -47,14 +47,6 @@ module GitlabRoutingHelper project_pipeline_path(pipeline.project, pipeline.id, *args) end - def milestone_path(entity, *args) - if entity.is_group_milestone? - group_milestone_path(entity.group, entity, *args) - elsif entity.is_project_milestone? - project_milestone_path(entity.project, entity, *args) - end - end - def issue_url(entity, *args) project_issue_url(entity.project, entity, *args) end @@ -67,14 +59,6 @@ module GitlabRoutingHelper project_pipeline_url(pipeline.project, pipeline.id, *args) end - def milestone_url(entity, *args) - if entity.is_group_milestone? - group_milestone_url(entity.group, entity, *args) - elsif entity.is_project_milestone? - project_milestone_url(entity.project, entity, *args) - end - end - def pipeline_job_url(pipeline, build, *args) project_job_url(pipeline.project, build.id, *args) end diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb new file mode 100644 index 00000000000..766d5262018 --- /dev/null +++ b/app/helpers/milestones_routing_helper.rb @@ -0,0 +1,17 @@ +module MilestonesRoutingHelper + def milestone_path(milestone, *args) + if milestone.is_group_milestone? + group_milestone_path(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_path(milestone.project, milestone, *args) + end + end + + def milestone_url(milestone, *args) + if milestone.is_group_milestone? + group_milestone_url(milestone.group, milestone, *args) + elsif milestone.is_project_milestone? + project_milestone_url(milestone.project, milestone, *args) + end + end +end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 48d00764965..01e0d0155a3 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -149,7 +149,9 @@ class Milestone < ActiveRecord::Base end ## - # Returns the String necessary to reference this Milestone in Markdown + # Returns the String necessary to reference this Milestone in Markdown. Group + # milestones only support name references, and do not support cross-project + # references. # # format - Symbol format to use (default: :iid, optional: :name) # @@ -161,12 +163,16 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid, full: false) - return if is_group_milestone? + return if is_group_milestone? && format != :name format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - "#{project.to_reference(from_project, full: full)}#{reference}" + if project + "#{project.to_reference(from_project, full: full)}#{reference}" + else + reference + end end def reference_link_text(from_project = nil) diff --git a/config/application.rb b/config/application.rb index f7145566262..47887bf8596 100644 --- a/config/application.rb +++ b/config/application.rb @@ -181,7 +181,11 @@ module Gitlab end end + # We add the MilestonesRoutingHelper because we know that this does not + # conflict with the methods defined in `project_url_helpers`, and we want + # these methods available in the same places. Gitlab::Routing.add_helpers(project_url_helpers) + Gitlab::Routing.add_helpers(MilestonesRoutingHelper) end end end diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 0d29b471d52..b42b8f0a525 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -248,7 +248,7 @@ GFM will recognize the following: | `~123` | label by ID | | `~bug` | one-word label by name | | `~"feature request"` | multi-word label by name | -| `%123` | milestone by ID | +| `%123` | project milestone by ID | | `%v1.23` | one-word milestone by name | | `%"release candidate"` | multi-word milestone by name | | `9ba12248` | specific commit | @@ -262,7 +262,7 @@ GFM also recognizes certain cross-project references: |:----------------------------------------|:------------------------| | `namespace/project#123` | issue | | `namespace/project!123` | merge request | -| `namespace/project%123` | milestone | +| `namespace/project%123` | project milestone | | `namespace/project$123` | snippet | | `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248...b19a04f5` | commit range comparison | @@ -274,7 +274,7 @@ It also has a shorthand version to reference other projects from the same namesp |:------------------------------|:------------------------| | `project#123` | issue | | `project!123` | merge request | -| `project%123` | milestone | +| `project%123` | project milestone | | `project$123` | snippet | | `project@9ba12248` | specific commit | | `project@9ba12248...b19a04f5` | commit range comparison | diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 685b43605ae..5db4fe77885 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -59,6 +59,12 @@ module Banzai # Example: project.merge_requests.find end + # Override if the link reference pattern produces a different ID (global + # ID vs internal ID, for instance) to the regular reference pattern. + def find_object_from_link(project, id) + find_object(project, id) + end + def find_object_cached(project, id) if RequestStore.active? cache = find_objects_cache[object_class][project.id] @@ -69,6 +75,16 @@ module Banzai end end + def find_object_from_link_cached(project, id) + if RequestStore.active? + cache = find_objects_from_link_cache[object_class][project.id] + + get_or_set_cache(cache, id) { find_object_from_link(project, id) } + else + find_object_from_link(project, id) + end + end + def project_from_ref_cached(ref) if RequestStore.active? cache = project_refs_cache @@ -120,7 +136,7 @@ module Banzai if link == inner_html && inner_html =~ /\A#{link_pattern}/ replace_link_node_with_text(node, link) do - object_link_filter(inner_html, link_pattern) + object_link_filter(inner_html, link_pattern, link_reference: true) end next @@ -128,7 +144,7 @@ module Banzai if link =~ /\A#{link_pattern}\z/ replace_link_node_with_href(node, link) do - object_link_filter(link, link_pattern, link_content: inner_html) + object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true) end next @@ -146,15 +162,26 @@ module Banzai # text - String text to replace references in. # pattern - Reference pattern to match against. # link_content - Original content of the link being replaced. + # link_reference - True if this was using the link reference pattern, + # false otherwise. # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text, pattern, link_content: nil) + def object_link_filter(text, pattern, link_content: nil, link_reference: false) references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| project_path = full_project_path(namespace_ref, project_ref) project = project_from_ref_cached(project_path) - if project && object = find_object_cached(project, id) + if project + object = + if link_reference + find_object_from_link_cached(project, id) + else + find_object_cached(project, id) + end + end + + if object title = object_link_title(object) klass = reference_class(object_sym) @@ -303,6 +330,12 @@ module Banzai end end + def find_objects_from_link_cache + RequestStore[:banzai_find_objects_from_link_cache] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + def url_for_object_cache RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key| hash[key] = Hash.new { |h, k| h[k] = {} } diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 45c033d32a8..4fc5f211e84 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -8,8 +8,15 @@ module Banzai Milestone end + # Links to project milestones contain the IID, but when we're handling + # 'regular' references, we need to use the global ID to disambiguate + # between group and project milestones. def find_object(project, id) - project.milestones.find_by(iid: id) + find_milestone_with_finder(project, id: id) + end + + def find_object_from_link(project, iid) + find_milestone_with_finder(project, iid: iid) end def references_in(text, pattern = Milestone.reference_pattern) @@ -22,7 +29,7 @@ module Banzai milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) if milestone - yield match, milestone.iid, $~[:project], $~[:namespace], $~ + yield match, milestone.id, $~[:project], $~[:namespace], $~ else match end @@ -36,7 +43,8 @@ module Banzai return unless project milestone_params = milestone_params(milestone_id, milestone_name) - project.milestones.find_by(milestone_params) + + find_milestone_with_finder(project, milestone_params) end def milestone_params(iid, name) @@ -47,15 +55,27 @@ module Banzai end end + def find_milestone_with_finder(project, params) + finder_params = { project_ids: [project.id], order: nil } + + # We don't support IID lookups for group milestones, because IIDs can + # clash between group and project milestones. + if project.group && !params[:iid] + finder_params[:group_ids] = [project.group.id] + end + + MilestonesFinder.new(finder_params).execute.find_by(params) + end + def url_for_object(milestone, project) - h = Gitlab::Routing.url_helpers - h.project_milestone_url(project, milestone, - only_path: context[:only_path]) + Gitlab::Routing + .url_helpers + .milestone_url(milestone, only_path: context[:only_path]) end def object_link_text(object, matches) milestone_link = escape_once(super) - reference = object.project.to_reference(project) + reference = object.project&.to_reference(project) if reference.present? "#{milestone_link} in #{reference}".html_safe diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 58b43805705..4f46e40ce7a 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -227,8 +227,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Milestone in another project: <%= xmilestone.to_reference(project) %> - Ignored in code: `<%= simple_milestone.to_reference %>` - Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) -- Milestone by URL: <%= urls.project_milestone_url(milestone.project, milestone) %> +- Milestone by URL: <%= urls.milestone_url(milestone) %> - Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) +- Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %> +- Group milestone by name in quotes: <%= group_milestone.to_reference(format: :name) %> +- Group milestone by URL is ignore: <%= urls.milestone_url(group_milestone) %> ### Task Lists diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb index 537e457513f..a44b200c5da 100644 --- a/spec/helpers/gitlab_routing_helper_spec.rb +++ b/spec/helpers/gitlab_routing_helper_spec.rb @@ -63,44 +63,4 @@ describe GitlabRoutingHelper do it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) } end end - - describe '#milestone_path' do - context 'for a group milestone' do - let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) } - - it 'links to the group milestone page' do - expect(milestone_path(milestone)) - .to eq(group_milestone_path(group, milestone)) - end - end - - context 'for a project milestone' do - let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) } - - it 'links to the project milestone page' do - expect(milestone_path(milestone)) - .to eq(project_milestone_path(project, milestone)) - end - end - end - - describe '#milestone_url' do - context 'for a group milestone' do - let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) } - - it 'links to the group milestone page' do - expect(milestone_url(milestone)) - .to eq(group_milestone_url(group, milestone)) - end - end - - context 'for a project milestone' do - let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) } - - it 'links to the project milestone page' do - expect(milestone_url(milestone)) - .to eq(project_milestone_url(project, milestone)) - end - end - end end diff --git a/spec/helpers/milestones_routing_helper_spec.rb b/spec/helpers/milestones_routing_helper_spec.rb new file mode 100644 index 00000000000..dc13a43c2ab --- /dev/null +++ b/spec/helpers/milestones_routing_helper_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe MilestonesRoutingHelper do + let(:project) { build_stubbed(:project) } + let(:group) { build_stubbed(:group) } + + describe '#milestone_path' do + context 'for a group milestone' do + let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) } + + it 'links to the group milestone page' do + expect(milestone_path(milestone)) + .to eq(group_milestone_path(group, milestone)) + end + end + + context 'for a project milestone' do + let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) } + + it 'links to the project milestone page' do + expect(milestone_path(milestone)) + .to eq(project_milestone_path(project, milestone)) + end + end + end + + describe '#milestone_url' do + context 'for a group milestone' do + let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) } + + it 'links to the group milestone page' do + expect(milestone_url(milestone)) + .to eq(group_milestone_url(group, milestone)) + end + end + + context 'for a project milestone' do + let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) } + + it 'links to the project milestone page' do + expect(milestone_url(milestone)) + .to eq(project_milestone_url(project, milestone)) + end + end + end +end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 5db77566513..ebd6c79077e 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -3,132 +3,25 @@ require 'spec_helper' describe Banzai::Filter::MilestoneReferenceFilter do include FilterSpecHelper - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - let(:reference) { milestone.to_reference } + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, group: group) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end - %w(pre code a style).each do |elem| - it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>milestone #{milestone.to_reference}" - expect(reference_filter(act).to_html).to eq exp - end - end - - it 'includes default classes' do - doc = reference_filter("Milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip' - end - - it 'includes a data-project attribute' do - doc = reference_filter("Milestone #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-project') - expect(link.attr('data-project')).to eq project.id.to_s - end - - it 'includes a data-milestone attribute' do - doc = reference_filter("See #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-milestone') - expect(link.attr('data-milestone')).to eq milestone.id.to_s - end - - it 'supports an :only_path context' do - doc = reference_filter("Milestone #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls - .project_milestone_path(project, milestone) - end - - context 'Integer-based references' do - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls - .project_milestone_url(project, milestone) + shared_examples 'reference parsing' do + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>milestone #{reference}" + expect(reference_filter(act).to_html).to eq exp + end end - it 'links with adjacent text' do - doc = reference_filter("Milestone (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) - end + it 'includes default classes' do + doc = reference_filter("Milestone #{reference}") - it 'ignores invalid milestone IIDs' do - exp = act = "Milestone #{invalidate_reference(reference)}" - - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'String-based single-word references' do - let(:milestone) { create(:milestone, name: 'gfm', project: project) } - let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls - .project_milestone_url(project, milestone) - expect(doc.text).to eq 'See gfm' - end - - it 'links with adjacent text' do - doc = reference_filter("Milestone (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) - end - - it 'ignores invalid milestone names' do - exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}" - - expect(reference_filter(act).to_html).to eq exp - end - end - - context 'String-based multi-word references in quotes' do - let(:milestone) { create(:milestone, name: 'gfm references', project: project) } - let(:reference) { milestone.to_reference(format: :name) } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls - .project_milestone_url(project, milestone) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = reference_filter("Milestone (#{reference}.)") - expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) - end - - it 'ignores invalid milestone names' do - exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}") - - expect(reference_filter(act).to_html).to eq exp - end - end - - describe 'referencing a milestone in a link href' do - let(:reference) { %Q{Milestone} } - - it 'links to a valid reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls - .project_milestone_url(project, milestone) - end - - it 'links with adjacent text' do - doc = reference_filter("Milestone (#{reference}.)") - expect(doc.to_html).to match(%r(\(Milestone\.\))) + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip' end it 'includes a data-project attribute' do @@ -146,9 +39,152 @@ describe Banzai::Filter::MilestoneReferenceFilter do expect(link).to have_attribute('data-milestone') expect(link.attr('data-milestone')).to eq milestone.id.to_s end + + it 'supports an :only_path context' do + doc = reference_filter("Milestone #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.milestone_path(milestone) + end end - describe 'cross-project / cross-namespace complete reference' do + shared_examples 'Integer-based references' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone IIDs' do + exp = act = "Milestone #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + shared_examples 'String-based single-word references' do + let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + + before do + milestone.update!(name: 'gfm') + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + shared_examples 'String-based multi-word references in quotes' do + let(:reference) { milestone.to_reference(format: :name) } + + before do + milestone.update!(name: 'gfm references') + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(#{milestone.name}\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + shared_examples 'referencing a milestone in a link href' do + let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + let(:link_reference) { %Q{Milestone} } + + before do + milestone.update!(name: 'gfm') + end + + it 'links to a valid reference' do + doc = reference_filter("See #{link_reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{link_reference}.)") + expect(doc.to_html).to match(%r(\(Milestone\.\))) + end + + it 'includes a data-project attribute' do + doc = reference_filter("Milestone #{link_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{link_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + end + + shared_examples 'linking to a milestone as the entire link' do + let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + let(:link) { urls.milestone_url(milestone) } + let(:link_reference) { %Q{#{link}} } + + it 'replaces the link text with the milestone reference' do + doc = reference_filter("See #{link}") + + expect(doc.css('a').first.text).to eq(unquoted_reference) + end + + it 'includes a data-project attribute' do + doc = reference_filter("Milestone #{link_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{link_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + end + + shared_examples 'cross-project / cross-namespace complete reference' do let(:namespace) { create(:namespace) } let(:another_project) { create(:project, :public, namespace: namespace) } let(:milestone) { create(:milestone, project: another_project) } @@ -184,7 +220,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do end end - describe 'cross-project / same-namespace complete reference' do + shared_examples 'cross-project / same-namespace complete reference' do let(:namespace) { create(:namespace) } let(:project) { create(:project, :public, namespace: namespace) } let(:another_project) { create(:project, :public, namespace: namespace) } @@ -221,7 +257,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do end end - describe 'cross project shorthand reference' do + shared_examples 'cross project shorthand reference' do let(:namespace) { create(:namespace) } let(:project) { create(:project, :public, namespace: namespace) } let(:another_project) { create(:project, :public, namespace: namespace) } @@ -258,27 +294,53 @@ describe Banzai::Filter::MilestoneReferenceFilter do end end - describe 'cross project milestone references' do - let(:another_project) { create(:project, :public) } - let(:project_path) { another_project.full_path } - let(:milestone) { create(:milestone, project: another_project) } - let(:reference) { milestone.to_reference(project) } + context 'project milestones' do + let(:milestone) { create(:milestone, project: project) } + let(:reference) { milestone.to_reference } - let!(:result) { reference_filter("See #{reference}") } + include_examples 'reference parsing' - it 'points to referenced project milestone page' do - expect(result.css('a').first.attr('href')).to eq urls - .project_milestone_url(another_project, milestone) + it_behaves_like 'Integer-based references' + it_behaves_like 'String-based single-word references' + it_behaves_like 'String-based multi-word references in quotes' + it_behaves_like 'referencing a milestone in a link href' + it_behaves_like 'cross-project / cross-namespace complete reference' + it_behaves_like 'cross-project / same-namespace complete reference' + it_behaves_like 'cross project shorthand reference' + end + + context 'group milestones' do + let(:milestone) { create(:milestone, group: group) } + let(:reference) { milestone.to_reference(format: :name) } + + include_examples 'reference parsing' + + it_behaves_like 'String-based single-word references' + it_behaves_like 'String-based multi-word references in quotes' + it_behaves_like 'referencing a milestone in a link href' + + it 'does not support references by IID' do + doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}") + + expect(doc.css('a')).to be_empty end - it 'contains cross project content' do - expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + it 'does not support references by link' do + doc = reference_filter("See #{urls.milestone_url(milestone)}") + + expect(doc.css('a').first.text).to eq(urls.milestone_url(milestone)) end - it 'escapes the name attribute' do - allow_any_instance_of(Milestone).to receive(:title).and_return(%{">whatever