diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce3..fc03d2580d7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -73,16 +73,23 @@ class Commit # This pattern supports cross-project references. def self.reference_pattern %r{ - (?:#{Project.reference_pattern}#{reference_prefix})? - (?\h{6,40}) + #{link_reference_pattern} | + (?: + (?:#{Project.reference_pattern}#{reference_prefix})? + (?\h{6,40}) + ) }x end + def self.link_reference_pattern + super("commit", /(?\h{6,40})/) + end + def to_reference(from_project = nil) if cross_project_reference?(from_project) - "#{project.to_reference}@#{id}" + project.to_reference + self.class.reference_prefix + self.short_id else - id + self.short_id end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index fd23e24aff6..98067771b71 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -42,11 +42,18 @@ class CommitRange # This pattern supports cross-project references. def self.reference_pattern %r{ - (?:#{Project.reference_pattern}#{reference_prefix})? - (?#{PATTERN}) + #{link_reference_pattern} | + (?: + (?:#{Project.reference_pattern}#{reference_prefix})? + (?#{PATTERN}) + ) }x end + def self.link_reference_pattern + super("compare", /(?#{PATTERN})/) + end + # Initialize a CommitRange # # range_string - The String commit range. diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index cced66cc1e4..16e4d054869 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -44,6 +44,25 @@ module Referable def reference_pattern raise NotImplementedError, "#{self} does not implement #{__method__}" end + + def link_reference_pattern(route, pattern) + %r{ + (? + #{Regexp.escape(Gitlab.config.gitlab.url)} + \/#{Project.reference_pattern} + \/#{Regexp.escape(route)} + \/#{pattern} + (? + (\/[a-z0-9_=-]+)* + )? + (? + \?[a-z0-9_=-]+ + (&[a-z0-9_=-]+)* + )? + (?\#[a-z0-9_-]+)? + ) + }x + end end private diff --git a/app/models/issue.rb b/app/models/issue.rb index 72183108033..e62acfdfd91 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -64,11 +64,18 @@ class Issue < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("issues", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 939df8cd8d1..c1d3874adee 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -146,11 +146,18 @@ class MergeRequest < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("merge_requests", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b0831982aa7..8ec12ddf6ef 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -60,11 +60,18 @@ class Snippet < ActiveRecord::Base # This pattern supports cross-project references. def self.reference_pattern %r{ - (#{Project.reference_pattern})? - #{Regexp.escape(reference_prefix)}(?\d+) + #{link_reference_pattern} | + (?: + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)}(?\d+) + ) }x end + def self.link_reference_pattern + super("snippets", /(?\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b082bfc434b..ee458eda025 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -181,8 +181,6 @@ module Gitlab Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, - Gitlab::Markdown::AutolinkFilter, - Gitlab::Markdown::ExternalLinkFilter, Gitlab::Markdown::UserReferenceFilter, Gitlab::Markdown::IssueReferenceFilter, @@ -193,6 +191,9 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, + Gitlab::Markdown::AutolinkFilter, + Gitlab::Markdown::ExternalLinkFilter, + Gitlab::Markdown::TaskListFilter ] end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index fd5b7eb9332..4adc44361b7 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -40,7 +40,7 @@ module Gitlab # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(object_class.reference_pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project] + yield match, $~[object_sym].to_i, $~[:project], $~ end end @@ -74,26 +74,41 @@ module Gitlab # 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) - references_in(text) do |match, id, project_ref| + references_in(text) do |match, id, project_ref, matches| project = project_from_ref(project_ref) if project && object = find_object(project, id) - title = escape_once("#{object_title}: #{object.title}") + title = escape_once(object_link_title(object)) klass = reference_class(object_sym) - data = data_attribute(project: project.id, object_sym => object.id) - url = url_for_object(object, project) + data = data_attribute(project: project.id, object_sym => object.id, original: match) + url = matches[:url] || url_for_object(object, project) + + text = object.to_reference(context[:project]) + + extras = object_link_text_extras(object, matches) + text += " (#{extras.join(", ")})" if extras.any? %(#{match}) + class="#{klass}">#{text}) else match end end end - def object_title - object_class.name.titleize + def object_link_text_extras(object, matches) + extras = [] + + if matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ + extras << "comment #{$1}" + end + + extras + end + + def object_link_title(object) + "#{object_class.name.titleize}: #{object.title}" end end end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index e070edae0a4..f24bed76193 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit range references with links. # # This filter supports cross-project references. - class CommitRangeReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitRangeReferenceFilter < AbstractReferenceFilter + def self.object_class + CommitRange + end - # Public: Find commit range references in text - # - # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref| - # "#{commit_range}" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit range, and an optional String - # of the external project reference. - # - # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(CommitRange.reference_pattern) do |match| - yield match, $~[:commit_range], $~[:project] + yield match, $~[:commit_range], $~[:project], $~ end end @@ -31,9 +21,9 @@ module Gitlab return unless project id = node.attr("data-commit-range") - range = CommitRange.new(id, project) + range = find_object(project, id) - return unless range.valid_commits? + return unless range { commit_range: range } end @@ -44,49 +34,25 @@ module Gitlab @commit_map = {} end - def call - replace_text_nodes_matching(CommitRange.reference_pattern) do |content| - commit_range_link_filter(content) - end + def self.find_object(project, id) + range = CommitRange.new(id, project) + + range.valid_commits? ? range : nil end - # Replace commit range references in text with links to compare the commit - # ranges. - # - # text - String text to replace references in. - # - # Returns a String with commit range references replaced with links. All - # links have `gfm` and `gfm-commit_range` class names attached for - # styling. - def commit_range_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - range = CommitRange.new(id, project) - - if range.valid_commits? - url = url_for_commit_range(project, range) - - title = range.reference_title - klass = reference_class(:commit_range) - data = data_attribute(project: project.id, commit_range: id) - - project_ref += '@' if project_ref - - %(#{project_ref}#{range}) - else - match - end - end + def find_object(*args) + self.class.find_object(*args) end - def url_for_commit_range(project, range) + def url_for_object(range, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end + + def object_link_title(range) + range.reference_title + end end end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index 8cdbeb1f9cf..cc7abc08c87 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit references with links. # # This filter supports cross-project references. - class CommitReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitReferenceFilter < AbstractReferenceFilter + def self.object_class + Commit + end - # Public: Find commit references in text - # - # CommitReferenceFilter.references_in(text) do |match, commit, project_ref| - # "#{commit}" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit identifier, and an optional - # String of the external project reference. - # - # Returns a String replaced with the return of the block. def self.references_in(text) text.gsub(Commit.reference_pattern) do |match| - yield match, $~[:commit], $~[:project] + yield match, $~[:commit], $~[:project], $~ end end @@ -31,58 +21,32 @@ module Gitlab return unless project id = node.attr("data-commit") - commit = commit_from_ref(project, id) + commit = find_object(project, id) return unless commit { commit: commit } end - def call - replace_text_nodes_matching(Commit.reference_pattern) do |content| - commit_link_filter(content) - end - end - - # Replace commit references in text with links to the commit specified. - # - # text - String text to replace references in. - # - # Returns a String with commit references replaced with links. All links - # have `gfm` and `gfm-commit` class names attached for styling. - def commit_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if commit = self.class.commit_from_ref(project, id) - url = url_for_commit(project, commit) - - title = escape_once(commit.link_title) - klass = reference_class(:commit) - data = data_attribute(project: project.id, commit: id) - - project_ref += '@' if project_ref - - %(#{project_ref}#{commit.short_id}) - else - match - end - end - end - - def self.commit_from_ref(project, id) + def self.find_object(project, id) if project && project.valid_repo? project.commit(id) end end - def url_for_commit(project, commit) + def find_object(*args) + self.class.find_object(*args) + end + + def url_for_object(commit, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end + + def object_link_title(commit) + commit.link_title + end end end end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb index 29e51b6ade6..b6792932016 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -10,6 +10,9 @@ module Gitlab doc.search('a').each do |node| next unless node.has_attribute?('href') + klass = node.attribute('class') + next if klass && klass.include?('gfm') + link = node.attribute('href').value # Skip non-HTTP(S) links diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 1f47f03c94e..3780a14a130 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -20,6 +20,16 @@ module Gitlab h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end + + def object_link_text_extras(object, matches) + extras = super + + if matches[:path] && matches[:path] == '/diffs' + extras.unshift "diffs" + end + + extras + end end end end diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index a1f3a8a8ebf..2a58c798f9f 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -12,7 +12,10 @@ module Gitlab def call doc.css('a.gfm').each do |node| unless user_can_reference?(node) - node.replace(node.text) + # The reference should be replaced by the original text, + # which is not always the same as the rendered text. + text = node.attribute('data-original') || node.text + node.replace(text) end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index e078ff26814..753140d60e6 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -125,7 +125,44 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + exp = Regexp.escape("#{project2.to_reference}@#{range.to_reference}") + expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape(range.to_reference(project)) expect(doc.to_html).to match(/\(#{exp}<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index d080efbf3d4..c1bcf29b1ba 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -131,5 +131,36 @@ module Gitlab::Markdown expect(result[:references][:commit]).not_to be_empty end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(#{commit.to_reference(project)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end end end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 94c80ae6611..296e8868f46 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -135,5 +135,37 @@ module Gitlab::Markdown expect(result[:references][:issue]).to eq [issue] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:get_issue). + with(issue.iid).and_return(nil) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 3ef6cdfff33..49d8a6e44da 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -89,7 +89,7 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } let(:reference) { merge.to_reference(project) } it 'links to a valid reference' do @@ -97,7 +97,7 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end it 'links with adjacent text' do @@ -116,5 +116,30 @@ module Gitlab::Markdown expect(result[:references][:merge_request]).to eq [merge] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } + let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, + project2, merge) + '/diffs#note_123' } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end end end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 9d9652dba46..3080a8a3608 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -114,5 +114,35 @@ module Gitlab::Markdown expect(result[:references][:snippet]).to eq [snippet] end end + + context 'URL cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" + + expect(filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end end end