Commit d6a5b45c authored by Douwe Maan's avatar Douwe Maan

Recognize issue/MR/snippet/commit links as references.

parent a7be01cd
...@@ -73,16 +73,23 @@ class Commit ...@@ -73,16 +73,23 @@ class Commit
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ %r{
(?:#{Project.reference_pattern}#{reference_prefix})? #{link_reference_pattern} |
(?<commit>\h{6,40}) (?:
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{6,40})
)
}x }x
end end
def self.link_reference_pattern
super("commit", /(?<commit>\h{6,40})/)
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
if cross_project_reference?(from_project) if cross_project_reference?(from_project)
"#{project.to_reference}@#{id}" project.to_reference + self.class.reference_prefix + self.short_id
else else
id self.short_id
end end
end end
......
...@@ -42,11 +42,18 @@ class CommitRange ...@@ -42,11 +42,18 @@ class CommitRange
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ %r{
(?:#{Project.reference_pattern}#{reference_prefix})? #{link_reference_pattern} |
(?<commit_range>#{PATTERN}) (?:
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{PATTERN})
)
}x }x
end end
def self.link_reference_pattern
super("compare", /(?<commit_range>#{PATTERN})/)
end
# Initialize a CommitRange # Initialize a CommitRange
# #
# range_string - The String commit range. # range_string - The String commit range.
......
...@@ -44,6 +44,25 @@ module Referable ...@@ -44,6 +44,25 @@ module Referable
def reference_pattern def reference_pattern
raise NotImplementedError, "#{self} does not implement #{__method__}" raise NotImplementedError, "#{self} does not implement #{__method__}"
end end
def link_reference_pattern(route, pattern)
%r{
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern}
\/#{Regexp.escape(route)}
\/#{pattern}
(?<path>
(\/[a-z0-9_=-]+)*
)?
(?<query>
\?[a-z0-9_=-]+
(&[a-z0-9_=-]+)*
)?
(?<anchor>\#[a-z0-9_-]+)?
)
}x
end
end end
private private
......
...@@ -64,11 +64,18 @@ class Issue < ActiveRecord::Base ...@@ -64,11 +64,18 @@ class Issue < ActiveRecord::Base
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ %r{
(#{Project.reference_pattern})? #{link_reference_pattern} |
#{Regexp.escape(reference_prefix)}(?<issue>\d+) (?:
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
)
}x }x
end end
def self.link_reference_pattern
super("issues", /(?<issue>\d+)/)
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -146,11 +146,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -146,11 +146,18 @@ class MergeRequest < ActiveRecord::Base
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ %r{
(#{Project.reference_pattern})? #{link_reference_pattern} |
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+) (?:
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
)
}x }x
end end
def self.link_reference_pattern
super("merge_requests", /(?<merge_request>\d+)/)
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -60,11 +60,18 @@ class Snippet < ActiveRecord::Base ...@@ -60,11 +60,18 @@ class Snippet < ActiveRecord::Base
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ %r{
(#{Project.reference_pattern})? #{link_reference_pattern} |
#{Regexp.escape(reference_prefix)}(?<snippet>\d+) (?:
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+)
)
}x }x
end end
def self.link_reference_pattern
super("snippets", /(?<snippet>\d+)/)
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
......
...@@ -181,8 +181,6 @@ module Gitlab ...@@ -181,8 +181,6 @@ module Gitlab
Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::RelativeLinkFilter,
Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::EmojiFilter,
Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::TableOfContentsFilter,
Gitlab::Markdown::AutolinkFilter,
Gitlab::Markdown::ExternalLinkFilter,
Gitlab::Markdown::UserReferenceFilter, Gitlab::Markdown::UserReferenceFilter,
Gitlab::Markdown::IssueReferenceFilter, Gitlab::Markdown::IssueReferenceFilter,
...@@ -193,6 +191,9 @@ module Gitlab ...@@ -193,6 +191,9 @@ module Gitlab
Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::CommitReferenceFilter,
Gitlab::Markdown::LabelReferenceFilter, Gitlab::Markdown::LabelReferenceFilter,
Gitlab::Markdown::AutolinkFilter,
Gitlab::Markdown::ExternalLinkFilter,
Gitlab::Markdown::TaskListFilter Gitlab::Markdown::TaskListFilter
] ]
end end
......
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(object_class.reference_pattern) do |match| text.gsub(object_class.reference_pattern) do |match|
yield match, $~[object_sym].to_i, $~[:project] yield match, $~[object_sym].to_i, $~[:project], $~
end end
end end
...@@ -74,26 +74,41 @@ module Gitlab ...@@ -74,26 +74,41 @@ module Gitlab
# Returns a String with references replaced with links. All links # Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text) 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) project = project_from_ref(project_ref)
if project && object = find_object(project, id) 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) klass = reference_class(object_sym)
data = data_attribute(project: project.id, object_sym => object.id) data = data_attribute(project: project.id, object_sym => object.id, original: match)
url = url_for_object(object, project) 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?
%(<a href="#{url}" #{data} %(<a href="#{url}" #{data}
title="#{title}" title="#{title}"
class="#{klass}">#{match}</a>) class="#{klass}">#{text}</a>)
else else
match match
end end
end end
end end
def object_title def object_link_text_extras(object, matches)
object_class.name.titleize 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 end
end end
......
...@@ -5,24 +5,14 @@ module Gitlab ...@@ -5,24 +5,14 @@ module Gitlab
# HTML filter that replaces commit range references with links. # HTML filter that replaces commit range references with links.
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitRangeReferenceFilter < ReferenceFilter class CommitRangeReferenceFilter < AbstractReferenceFilter
include CrossProjectReference def self.object_class
CommitRange
end
# Public: Find commit range references in text
#
# CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
# "<a href=...>#{commit_range}</a>"
# 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) def self.references_in(text)
text.gsub(CommitRange.reference_pattern) do |match| text.gsub(CommitRange.reference_pattern) do |match|
yield match, $~[:commit_range], $~[:project] yield match, $~[:commit_range], $~[:project], $~
end end
end end
...@@ -31,9 +21,9 @@ module Gitlab ...@@ -31,9 +21,9 @@ module Gitlab
return unless project return unless project
id = node.attr("data-commit-range") 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 } { commit_range: range }
end end
...@@ -44,49 +34,25 @@ module Gitlab ...@@ -44,49 +34,25 @@ module Gitlab
@commit_map = {} @commit_map = {}
end end
def call def self.find_object(project, id)
replace_text_nodes_matching(CommitRange.reference_pattern) do |content| range = CommitRange.new(id, project)
commit_range_link_filter(content)
end
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 range.valid_commits? ? range : nil
end
%(<a href="#{url}" #{data} def find_object(*args)
title="#{title}" self.class.find_object(*args)
class="#{klass}">#{project_ref}#{range}</a>)
else
match
end
end
end end
def url_for_commit_range(project, range) def url_for_object(range, project)
h = Gitlab::Application.routes.url_helpers h = Gitlab::Application.routes.url_helpers
h.namespace_project_compare_url(project.namespace, project, h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path])) range.to_param.merge(only_path: context[:only_path]))
end end
def object_link_title(range)
range.reference_title
end
end end
end end
end end
...@@ -5,24 +5,14 @@ module Gitlab ...@@ -5,24 +5,14 @@ module Gitlab
# HTML filter that replaces commit references with links. # HTML filter that replaces commit references with links.
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitReferenceFilter < ReferenceFilter class CommitReferenceFilter < AbstractReferenceFilter
include CrossProjectReference def self.object_class
Commit
end
# Public: Find commit references in text
#
# CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
# "<a href=...>#{commit}</a>"
# 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) def self.references_in(text)
text.gsub(Commit.reference_pattern) do |match| text.gsub(Commit.reference_pattern) do |match|
yield match, $~[:commit], $~[:project] yield match, $~[:commit], $~[:project], $~
end end
end end
...@@ -31,58 +21,32 @@ module Gitlab ...@@ -31,58 +21,32 @@ module Gitlab
return unless project return unless project
id = node.attr("data-commit") id = node.attr("data-commit")
commit = commit_from_ref(project, id) commit = find_object(project, id)
return unless commit return unless commit
{ commit: commit } { commit: commit }
end end
def call def self.find_object(project, id)
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
%(<a href="#{url}" #{data}
title="#{title}"
class="#{klass}">#{project_ref}#{commit.short_id}</a>)
else
match
end
end
end
def self.commit_from_ref(project, id)
if project && project.valid_repo? if project && project.valid_repo?
project.commit(id) project.commit(id)
end end
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 = Gitlab::Application.routes.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit, h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path]) only_path: context[:only_path])
end end
def object_link_title(commit)
commit.link_title
end
end end
end end
end end
...@@ -10,6 +10,9 @@ module Gitlab ...@@ -10,6 +10,9 @@ module Gitlab
doc.search('a').each do |node| doc.search('a').each do |node|
next unless node.has_attribute?('href') next unless node.has_attribute?('href')
klass = node.attribute('class')
next if klass && klass.include?('gfm')
link = node.attribute('href').value link = node.attribute('href').value
# Skip non-HTTP(S) links # Skip non-HTTP(S) links
......
...@@ -20,6 +20,16 @@ module Gitlab ...@@ -20,6 +20,16 @@ module Gitlab
h.namespace_project_merge_request_url(project.namespace, project, mr, h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path]) only_path: context[:only_path])
end 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 end
end end
...@@ -12,7 +12,10 @@ module Gitlab ...@@ -12,7 +12,10 @@ module Gitlab
def call def call
doc.css('a.gfm').each do |node| doc.css('a.gfm').each do |node|
unless user_can_reference?(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
end end
......
...@@ -125,7 +125,44 @@ module Gitlab::Markdown ...@@ -125,7 +125,44 @@ module Gitlab::Markdown
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)") 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(/\(<a.+>#{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(/\(<a.+>#{exp}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end end
......
...@@ -131,5 +131,36 @@ module Gitlab::Markdown ...@@ -131,5 +131,36 @@ module Gitlab::Markdown
expect(result[:references][:commit]).not_to be_empty expect(result[:references][:commit]).not_to be_empty
end end
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(/\(<a.+>#{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
end end
...@@ -135,5 +135,37 @@ module Gitlab::Markdown ...@@ -135,5 +135,37 @@ module Gitlab::Markdown
expect(result[:references][:issue]).to eq [issue] expect(result[:references][:issue]).to eq [issue]
end end
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(/\(<a.+>#{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
end end
...@@ -89,7 +89,7 @@ module Gitlab::Markdown ...@@ -89,7 +89,7 @@ module Gitlab::Markdown
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, :public, namespace: namespace) } 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) } let(:reference) { merge.to_reference(project) }
it 'links to a valid reference' do it 'links to a valid reference' do
...@@ -97,7 +97,7 @@ module Gitlab::Markdown ...@@ -97,7 +97,7 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_merge_request_url(project2.namespace, to eq urls.namespace_project_merge_request_url(project2.namespace,
project, merge) project2, merge)
end end
it 'links with adjacent text' do it 'links with adjacent text' do
...@@ -116,5 +116,30 @@ module Gitlab::Markdown ...@@ -116,5 +116,30 @@ module Gitlab::Markdown
expect(result[:references][:merge_request]).to eq [merge] expect(result[:references][:merge_request]).to eq [merge]
end end
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(/\(<a.+>#{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
end end
...@@ -114,5 +114,35 @@ module Gitlab::Markdown ...@@ -114,5 +114,35 @@ module Gitlab::Markdown
expect(result[:references][:snippet]).to eq [snippet] expect(result[:references][:snippet]).to eq [snippet]
end end
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(/\(<a.+>#{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
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