diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c23921f28bd7c1005dee8ad4e8157c2eadd87b62..f31873c6b85db5b644ac605036e717f08f9a5588 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -259,7 +259,7 @@ def to_reference(from = nil, format: :name, full: false) if project "#{project.to_reference_base(from, full: full)}#{reference}" else - reference + "#{group.to_reference_base(from, full: full)}#{reference}" end end diff --git a/app/models/project.rb b/app/models/project.rb index b338eb500f5758223960c254ff860f4633bf503f..140c5905620091c4ad8abed5fc3ebb2ab55dd512 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -988,6 +988,7 @@ def sort_by_attribute(method) def reference_pattern %r{ (?/)? ((?#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})/)? (?#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX}) }xo diff --git a/doc/user/markdown.md b/doc/user/markdown.md index eb7c6cc2b46463136924079941b62ab2a06c8401..719596e2e0582e068bf94a9d7ac47b91c994ce58 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -712,9 +712,9 @@ GitLab Flavored Markdown recognizes the following: | Label by name (one word) | `~bug` | `namespace/project~bug` | `project~bug` | | Label by name (multiple words) | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` | | Label by name (scoped) | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` | -| Project milestone by ID | `%123` | `namespace/project%123` | `project%123` | -| Milestone by name (one word) | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` | -| Milestone by name (multiple words) | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` | +| Project milestone by ID 2 | `%123` | `namespace/project%123` | `project%123` | +| Milestone by name (one word) 2 | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` | +| Milestone by name (multiple words) 2 | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` | | Commit (specific) | `9ba12248` | `namespace/project@9ba12248` | `project@9ba12248` | | Commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` | | Repository file reference | `[README](doc/README.md)` | | | @@ -722,14 +722,14 @@ GitLab Flavored Markdown recognizes the following: | [Alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` | | [Contact](crm/index.md#contacts) | `[contact:test@example.com]` | | | -
    -
  1. - - Introduced in GitLab 16.9. Iteration cadence references are always rendered following the format [cadence:<ID>]. - For example, the text reference [cadence:"plan"] renders as [cadence:1] if the referenced iterations cadence's ID is 1. - -
  2. -
+**Footnotes:** + +1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384885) in GitLab 16.9. + Iteration cadence references are always rendered following the format `[cadence:]`. + For example, the text reference `[cadence:"plan"]` renders as `[cadence:1]` if the referenced + iterations cadence's ID is `1`. +1. For milestones, prepend a `/` before `namespace/project` to specify the exact milestone, + removing any possible ambiguity. For example, referencing an issue by using `#123` formats the output as a link to issue number 123 with text `#123`. Likewise, a link to issue number 123 is diff --git a/ee/spec/models/iteration_spec.rb b/ee/spec/models/iteration_spec.rb index 818e65a566ee4ac7c766529e8385484bc48dd21c..2f460b7c8820e2d0178c69df0634e75f3e458320 100644 --- a/ee/spec/models/iteration_spec.rb +++ b/ee/spec/models/iteration_spec.rb @@ -71,7 +71,8 @@ 'namespace' => 'gitlab-org', 'project' => 'gitlab-ce', 'iteration_id' => '123', - 'iteration_name' => nil + 'iteration_name' => nil, + 'absolute_path' => nil ) end end @@ -84,7 +85,8 @@ 'namespace' => 'gitlab-org', 'project' => 'gitlab-ce', 'iteration_id' => nil, - 'iteration_name' => 'my-iteration' + 'iteration_name' => 'my-iteration', + 'absolute_path' => nil ) end end @@ -97,7 +99,8 @@ 'namespace' => 'gitlab-org', 'project' => 'gitlab-ce', 'iteration_id' => nil, - 'iteration_name' => 'my-iteration' + 'iteration_name' => 'my-iteration', + 'absolute_path' => nil ) end end diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb index c3c5103106b510325d9d22cc4144f89eb1c16640..a8014937ea5f303fae1c46d310a9f4e2db0ed24a 100644 --- a/lib/banzai/filter/references/abstract_reference_filter.rb +++ b/lib/banzai/filter/references/abstract_reference_filter.rb @@ -184,7 +184,7 @@ def object_link_filter(text, pattern, link_content: nil, link_reference: false) parent_path = if parent_type == :group reference_cache.full_group_path(namespace_ref) else - reference_cache.full_project_path(namespace_ref, project_ref) + reference_cache.full_project_path(namespace_ref, project_ref, matches) end parent = from_ref_cached(parent_path) diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb index 6020c7b7f58084b5fe74adb31e4b88fe4ddf7b4e..170fdd92c677e6a97b710d48e6384bde603e7a06 100644 --- a/lib/banzai/filter/references/label_reference_filter.rb +++ b/lib/banzai/filter/references/label_reference_filter.rb @@ -109,7 +109,7 @@ def object_link_text(object, matches) parent = project || group if project || full_path_ref?(matches) - project_path = reference_cache.full_project_path(matches[:namespace], matches[:project]) + project_path = reference_cache.full_project_path(matches[:namespace], matches[:project], matches) parent_from_ref = from_ref_cached(project_path) reference = parent_from_ref.to_human_reference(parent) diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb index 77658f72d34a4efef49000822e678253d352ff40..af8050cf8d001cffc2c62ec64786855d12d62f05 100644 --- a/lib/banzai/filter/references/milestone_reference_filter.rb +++ b/lib/banzai/filter/references/milestone_reference_filter.rb @@ -11,17 +11,24 @@ class MilestoneReferenceFilter < AbstractReferenceFilter def parent_records(parent, ids) return Milestone.none unless valid_context?(parent) - milestone_iids = ids.map { |y| y[:milestone_iid] }.compact - unless milestone_iids.empty? - iid_relation = find_milestones(parent, true).where(iid: milestone_iids) - end + relation = [] + + # We need to handle relative and absolute paths separately + milestones_absolute_indexed = ids.group_by { |id| id[:absolute_path] } + milestones_absolute_indexed.each do |absolute_path, fitered_ids| + milestone_iids = fitered_ids&.pluck(:milestone_iid)&.compact + + if milestone_iids.present? + relation << find_milestones(parent, true, absolute_path: absolute_path).where(iid: milestone_iids) + end - milestone_names = ids.map { |y| y[:milestone_name] }.compact - unless milestone_names.empty? - milestone_relation = find_milestones(parent, false).where(name: milestone_names) + milestone_names = fitered_ids&.pluck(:milestone_name)&.compact + if milestone_names.present? + relation << find_milestones(parent, false, absolute_path: absolute_path).where(name: milestone_names) + end end - relation = [iid_relation, milestone_relation].compact + relation.compact! return Milestone.none if relation.all?(Milestone.none) Milestone.from_union(relation).includes(:project, :group) @@ -44,12 +51,14 @@ def find_object(parent_object, id) # or the milestone_name, but not both. But below, we have both pieces of information. # But it's accounted for in `find_object` def parse_symbol(symbol, match_data) + absolute_path = !!match_data&.named_captures&.fetch('absolute_path') + if symbol # when parsing links, there is no `match_data[:milestone_iid]`, but `symbol` # holds the iid - { milestone_iid: symbol.to_i, milestone_name: nil } + { milestone_iid: symbol.to_i, milestone_name: nil, absolute_path: absolute_path } else - { milestone_iid: match_data[:milestone_iid]&.to_i, milestone_name: match_data[:milestone_name]&.tr('"', '') } + { milestone_iid: match_data[:milestone_iid]&.to_i, milestone_name: match_data[:milestone_name]&.tr('"', ''), absolute_path: absolute_path } end end @@ -97,27 +106,27 @@ def references_in(text, pattern = Milestone.reference_pattern) escape_with_placeholders(unescaped_html, milestones) end - def find_milestones(parent, find_by_iid = false) - finder_params = milestone_finder_params(parent, find_by_iid) + def find_milestones(parent, find_by_iid = false, absolute_path: false) + finder_params = milestone_finder_params(parent, find_by_iid, absolute_path) MilestonesFinder.new(finder_params).execute end - def milestone_finder_params(parent, find_by_iid) + def milestone_finder_params(parent, find_by_iid, absolute_path) { order: nil, state: 'all' }.tap do |params| params[:project_ids] = parent.id if project_context?(parent) # We don't support IID lookups because IIDs can clash between # group/project milestones and group/subgroup milestones. - params[:group_ids] = group_and_ancestors_ids(parent) unless find_by_iid + params[:group_ids] = group_and_ancestors_ids(parent, absolute_path) unless find_by_iid end end - def group_and_ancestors_ids(parent) + def group_and_ancestors_ids(parent, absolute_path) if group_context?(parent) - parent.self_and_ancestors.select(:id) + absolute_path ? parent.id : parent.self_and_ancestors.select(:id) elsif project_context?(parent) - parent.group&.self_and_ancestors&.select(:id) + absolute_path ? nil : parent.group&.self_and_ancestors&.select(:id) end end diff --git a/lib/banzai/filter/references/reference_cache.rb b/lib/banzai/filter/references/reference_cache.rb index 9bddd0c376de9c10e7b0973f63e5c9e1bc110e29..3878d0a3bf1a39447179bae49a74d796f37018e9 100644 --- a/lib/banzai/filter/references/reference_cache.rb +++ b/lib/banzai/filter/references/reference_cache.rb @@ -45,11 +45,17 @@ def current_project_namespace_path end end - def full_project_path(namespace, project_ref) + def full_project_path(namespace, project_ref, matches = nil) return current_parent_path unless project_ref - namespace_ref = namespace || current_project_namespace_path - "#{namespace_ref}/#{project_ref}" + matched_absolute_path = matches&.named_captures&.fetch('absolute_path') + namespace ||= current_project_namespace_path unless matched_absolute_path + + full_path = [] + full_path << '/' if matched_absolute_path + full_path << "#{namespace}/" if namespace + full_path << project_ref + full_path.join end def full_group_path(group_ref) @@ -80,10 +86,10 @@ def load_references_per_parent(nodes) next unless pattern prepare_doc_for_scan.to_enum(:scan, pattern).each do - parent_path = if parent_type == :project - full_project_path($~[:namespace], $~[:project]) - else + parent_path = if parent_type == :group full_group_path($~[:group]) + else + full_project_path($~[:namespace], $~[:project], $~) end ident = filter.identifier($~) @@ -101,7 +107,7 @@ def load_parent_per_reference @per_reference ||= {} @per_reference[parent_type] ||= begin - refs = references_per_parent.keys + refs = references_per_parent.keys.compact parent_ref = {} # if we already have a parent, no need to query it again @@ -117,7 +123,12 @@ def load_parent_per_reference refs -= [ref] if parent_ref[ref] end - find_for_paths(refs).index_by(&:full_path).merge(parent_ref) + absolute_paths = refs.filter_map { |ref| ref if ref[0] == '/' } + relative_paths = refs - absolute_paths + + find_for_paths(relative_paths, false).index_by(&:full_path) + .merge(find_for_paths(absolute_paths, true).index_by { |object| "/#{object.full_path}" }) + .merge(parent_ref) end end @@ -140,41 +151,60 @@ def load_records_per_parent end # Returns projects for the given paths. - def find_for_paths(paths) + def find_for_paths(paths, absolute_path = false) + return [] if paths.empty? + if Gitlab::SafeRequestStore.active? - cache = refs_cache - to_query = paths - cache.keys + cached_objects_for_paths(paths, absolute_path) + else + objects_for_paths(paths, absolute_path) + end + end - unless to_query.empty? - records = objects_for_paths(to_query) + def cached_objects_for_paths(paths, absolute_path) + cache = refs_cache + to_query = paths - cache.keys - found = [] - records.each do |record| - ref = record.full_path - get_or_set_cache(cache, ref) { record } - found << ref - end + unless to_query.empty? + records = objects_for_paths(to_query, absolute_path) - not_found = to_query - found - not_found.each do |ref| - get_or_set_cache(cache, ref) { nil } - end + found = [] + records.each do |record| + ref = absolute_path ? "/#{record.full_path}" : record.full_path + get_or_set_cache(cache, ref) { record } + found << ref end - cache.slice(*paths).values.compact - else - objects_for_paths(paths) + not_found = to_query - found + not_found.each do |ref| + get_or_set_cache(cache, ref) { nil } + end end + + cache.slice(*paths).values.compact end - def objects_for_paths(paths) + def objects_for_paths(paths, absolute_path) + search_paths = absolute_path ? paths.pluck(1..-1) : paths + klass = parent_type.to_s.camelize.constantize - result = klass.where_full_path_in(paths) + result = klass.where_full_path_in(search_paths) return result if parent_type == :group return unless parent_type == :project - result.includes(namespace: :route) + projects = result.includes(namespace: :route) .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046") + + return projects unless absolute_path + + # If we make it to here, then we're handling absolute path(s). + # Which means we need to also search groups as well as projects. + # Possible future optimization might be to use Route along the lines of: + # Routable.where_full_path_in(paths).includes(:source) + # See `routable.rb` + groups = Group.where_full_path_in(search_paths) + + projects.to_a + groups.to_a end def refs_cache diff --git a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb index e778f07227c7b77140576f4a5427b406c1334688..49d591d23bfc6a3161707c2ce854b52f363555d8 100644 --- a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb @@ -323,6 +323,18 @@ end end + shared_examples 'absolute references' do + it 'supports absolute reference' do + absolute_reference = "/#{reference}" + + result = reference_filter("See #{absolute_reference}") + + expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) + expect(result.css('a').first.attr('data-original')).to eq absolute_reference + expect(result.content).to eq "See %#{milestone.title}" + end + end + shared_context 'project milestones' do let(:reference) { milestone.to_reference(format: :iid) } @@ -341,6 +353,10 @@ let(:resource) { milestone } let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } end + + it_behaves_like 'absolute references' do + let(:reference) { milestone.to_reference(format: :iid, full: true) } + end end shared_context 'group milestones' do @@ -357,6 +373,10 @@ let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } end + it_behaves_like 'absolute references' do + let(:reference) { milestone.to_reference(format: :name, full: true) } + end + it 'does not support references by IID' do doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}") @@ -411,6 +431,10 @@ expect(reference_filter(act, context).to_html).to eq exp end + + it_behaves_like 'absolute references' do + let(:reference) { "#{project.full_path}%#{milestone.iid}" } + end end context 'when group milestone' do @@ -420,7 +444,7 @@ let(:sub_group) { create(:group, parent: group) } let(:sub_group_milestone) { create(:milestone, title: 'sub_group_milestone', group: sub_group) } - it 'links to a valid reference of subgroup and group milestones' do + it 'links to valid references of subgroup and group milestones' do [group_milestone, sub_group_milestone].each do |milestone| reference = "%#{milestone.title}" @@ -429,6 +453,18 @@ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) end end + + it 'links to valid absolute references of subgroup and group milestones' do + [group_milestone, sub_group_milestone].each do |milestone| + reference = "/#{milestone.group.full_path}%#{milestone.title}" + + result = reference_filter("See #{reference}", { project: nil, group: sub_group }) + + expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) + expect(result.css('a').first.attr('data-original')).to eq reference + expect(result.content).to eq "See %#{milestone.title}" + end + end end it 'ignores internal references' do @@ -450,6 +486,36 @@ expect(links[1].attr('href')).to eq(urls.milestone_url(group_milestone)) end end + + context 'when referencing both project and group milestones using absolute references' do + let(:milestone) { create(:milestone, project: project) } + let(:group_milestone) { create(:milestone, title: 'group_milestone', group: project.group) } + + it 'links to valid references' do + doc = reference_filter("See /#{milestone.to_reference(full: true)} and /#{group_milestone.to_reference(full: true)}", context) + links = doc.css('a') + + expect(links.length).to eq(2) + expect(links[0].attr('href')).to eq(urls.milestone_url(milestone)) + expect(links[1].attr('href')).to eq(urls.milestone_url(group_milestone)) + end + end + + context 'when referencing both group and subgroup milestones using absolute references' do + let(:subgroup) { create(:group, :public, parent: group) } + let(:group_milestone) { create(:milestone, title: 'group_milestone', group: group) } + let(:subgroup_milestone) { create(:milestone, title: 'group_milestone', group: subgroup) } + let(:context) { { project: project, group: nil } } + + it 'links to valid references' do + doc = reference_filter("See /#{group_milestone.to_reference(full: true)} and /#{subgroup_milestone.to_reference(full: true)}", context) + links = doc.css('a') + + expect(links.length).to eq(2) + expect(links[0].attr('href')).to eq(urls.milestone_url(group_milestone)) + expect(links[1].attr('href')).to eq(urls.milestone_url(subgroup_milestone)) + end + end end context 'when milestone is open' do diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb index e45b798823ab6f0a90736dc1316ac72e2f74c3d1..7fc9f0f2652a8d4b4a25172e6464861d77b7e8b9 100644 --- a/spec/lib/banzai/filter/references/reference_cache_spec.rb +++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb @@ -3,11 +3,14 @@ require 'spec_helper' RSpec.describe Banzai::Filter::References::ReferenceCache, feature_category: :team_planning do - let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: group) } let_it_be(:project2) { create(:project) } let_it_be(:issue1) { create(:issue, project: project) } let_it_be(:issue2) { create(:issue, project: project) } let_it_be(:issue3) { create(:issue, project: project2) } + let_it_be(:issue4) { create(:issue, project: project2) } let_it_be(:doc) { Nokogiri::HTML.fragment("#{issue1.to_reference} #{issue2.to_reference} #{issue3.to_reference(full: true)}") } let_it_be(:result) { {} } let_it_be(:filter_class) { Banzai::Filter::References::IssueReferenceFilter } @@ -63,17 +66,49 @@ describe '#parent_per_reference' do it 'returns a Hash containing projects grouped per parent paths' do - expect(cache.parent_per_reference).to match({ project.full_path => project, project2.full_path => project2 }) + expect(cache.parent_per_reference).to include({ project.full_path => project, project2.full_path => project2 }) end end describe '#records_per_parent' do - it 'returns a Hash containing projects grouped per parent paths' do + it 'returns a Hash containing records grouped per parent' do expect(cache.records_per_parent).to match({ project => { issue1.iid => issue1, issue2.iid => issue2 }, project2 => { issue3.iid => issue3 } }) end end end + + context 'when the cache is loaded with absolute references' do + it 'loads references grouped per parent path and absolute references' do + milestone1 = create(:milestone, group: group) + milestone2 = create(:milestone, group: subgroup) + milestone3 = create(:milestone, project: project) + + doc_milestone = Nokogiri::HTML.fragment("/#{milestone1.to_reference(full: true)} /#{milestone2.to_reference(full: true)} #{milestone3.to_reference(full: true)}") + filter_milestone = Banzai::Filter::References::MilestoneReferenceFilter.new(doc_milestone, project: project) + cache_milestone = described_class.new(filter_milestone, { project: project }, {}) + + cache_milestone.load_reference_cache(filter_milestone.nodes) + + expect(cache_milestone.references_per_parent).to match({ + "/#{group.full_path}" => [{ milestone_iid: nil, milestone_name: milestone1.title, absolute_path: true }].to_set, + "/#{subgroup.full_path}" => [{ milestone_iid: nil, milestone_name: milestone2.title, absolute_path: true }].to_set, + project.full_path => [{ milestone_iid: nil, milestone_name: milestone3.title, absolute_path: false }].to_set + }) + + expect(cache_milestone.parent_per_reference).to match({ + "/#{group.full_path}" => group, + "/#{subgroup.full_path}" => subgroup, + project.full_path => project + }) + + expect(cache_milestone.records_per_parent).to match({ + group => { { milestone_iid: milestone1.iid, milestone_name: milestone1.title } => milestone1 }, + subgroup => { { milestone_iid: milestone2.iid, milestone_name: milestone2.title } => milestone2 }, + project => { { milestone_iid: milestone3.iid, milestone_name: milestone3.title } => milestone3 } + }) + end + end end describe '#initialize_reference_cache' do @@ -105,8 +140,8 @@ end describe '#find_for_paths' do - def find_for_paths(paths) - cache.send(:find_for_paths, paths) + def find_for_paths(paths, absolute_path = false) + cache.send(:find_for_paths, paths, absolute_path) end context 'with RequestStore disabled' do @@ -117,6 +152,14 @@ def find_for_paths(paths) it 'return an empty array for paths that do not exist' do expect(find_for_paths(['nonexistent/project'])).to eq([]) end + + it 'finds group and project by absolute path' do + project_path = "/#{project.full_path}" + group_path = "/#{subgroup.full_path}" + nonexistent_path = '/nonexistent/project' + + expect(find_for_paths([project_path, group_path, nonexistent_path], true)).to match_array([project, subgroup]) + end end context 'with RequestStore enabled', :request_store do @@ -166,9 +209,16 @@ def find_for_paths(paths) expect(cache.full_project_path('something', 'cool')).to eq 'something/cool' end - it 'returns uses default namespace and project ref when namespace nil' do + it 'returns default namespace and project ref when namespace nil' do expect(cache.full_project_path(nil, 'cool')).to eq "#{project.namespace.full_path}/cool" end + + it 'returns absolute paths when matched to an absolute path' do + match = "/something/cool".match(Project.reference_pattern) + + expect(cache.full_project_path('something', 'cool', match)).to eq '/something/cool' + expect(cache.full_project_path(nil, 'cool', match)).to eq '/cool' + end end describe '#full_group_path' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 79b663807b4f75838c63878789a6b690fc3d04c1..a429a7d9cefd67c3a98eaeeb06b02ab3912a0f85 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -470,7 +470,7 @@ end it 'does supports cross-project references within a group' do - expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"' + expect(milestone.to_reference(another_project, format: :name)).to eq "#{group.full_path}%\"milestone\"" end it 'raises an error when using iid format' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 993cbd8e23f4d8297ad599112907335dd17af4ee..bb10e6010afb6387e2f7c809c5504fbece33f7b8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1491,6 +1491,26 @@ end end end + + describe '#reference_pattern' do + it 'matches a normal reference' do + reference = project.to_reference + match = reference.match(described_class.reference_pattern) + + expect(match[:namespace]).to eq project.namespace.full_path + expect(match[:project]).to eq project.path + expect(match[:absolute_path]).to eq nil + end + + it 'matches an absolute reference' do + reference = "/#{project.to_reference}" + match = reference.match(described_class.reference_pattern) + + expect(match[:namespace]).to eq project.namespace.full_path + expect(match[:project]).to eq project.path + expect(match[:absolute_path]).to eq '/' + end + end end describe '#to_reference_base' do