Commit 4cb1cc2b authored by Robert Speicher's avatar Robert Speicher

Make CommitRange and Snippets cross-referable

parent d520fe07
...@@ -7,11 +7,14 @@ module Gitlab ...@@ -7,11 +7,14 @@ module Gitlab
# snippets that do not exist are ignored. # snippets that do not exist are ignored.
# #
# Context options: # Context options:
# :project (required) - Current project. # :project (required) - Current project, ignored when reference is
# cross-project.
# :reference_class - Custom CSS class added to reference links. # :reference_class - Custom CSS class added to reference links.
# :only_path - Generate path-only links. # :only_path - Generate path-only links.
# #
class SnippetReferenceFilter < HTML::Pipeline::Filter class SnippetReferenceFilter < HTML::Pipeline::Filter
include CrossProjectReference
# Public: Find `$123` snippet references in text # Public: Find `$123` snippet references in text
# #
# SnippetReferenceFilter.references_in(text) do |match, snippet| # SnippetReferenceFilter.references_in(text) do |match, snippet|
...@@ -20,17 +23,20 @@ module Gitlab ...@@ -20,17 +23,20 @@ module Gitlab
# #
# text - String text to search. # text - String text to search.
# #
# Yields the String match and the Integer snippet ID. # Yields the String match, the Integer snippet ID, and an optional String
# of the external project reference.
# #
# 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(SNIPPET_PATTERN) do |match| text.gsub(SNIPPET_PATTERN) do |match|
yield match, $~[:snippet].to_i yield match, $~[:snippet].to_i, $~[:project]
end end
end end
# Pattern used to extract `$123` snippet references from text # Pattern used to extract `$123` snippet references from text
SNIPPET_PATTERN = /\$(?<snippet>\d+)/ #
# This pattern supports cross-project references.
SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
# Don't look for references in text nodes that are children of these # Don't look for references in text nodes that are children of these
# elements. # elements.
...@@ -40,7 +46,7 @@ module Gitlab ...@@ -40,7 +46,7 @@ module Gitlab
doc.search('text()').each do |node| doc.search('text()').each do |node|
content = node.to_html content = node.to_html
next if project.nil? next if context[:project].nil?
next unless content.match(SNIPPET_PATTERN) next unless content.match(SNIPPET_PATTERN)
next if has_ancestor?(node, IGNORE_PARENTS) next if has_ancestor?(node, IGNORE_PARENTS)
...@@ -66,9 +72,9 @@ module Gitlab ...@@ -66,9 +72,9 @@ module Gitlab
# Returns a String with `$123` references replaced with links. All links # Returns a String with `$123` references replaced with links. All links
# have `gfm` and `gfm-snippet` class names attached for styling. # have `gfm` and `gfm-snippet` class names attached for styling.
def snippet_link_filter(text) def snippet_link_filter(text)
project = context[:project] self.class.references_in(text) do |match, id, project_ref|
project = self.project_from_ref(project_ref)
self.class.references_in(text) do |match, id|
if snippet = project.snippets.find_by(id: id) if snippet = project.snippets.find_by(id: id)
title = "Snippet: #{snippet.title}" title = "Snippet: #{snippet.title}"
klass = "gfm gfm-snippet #{context[:reference_class]}".strip klass = "gfm gfm-snippet #{context[:reference_class]}".strip
...@@ -77,17 +83,13 @@ module Gitlab ...@@ -77,17 +83,13 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">$#{id}</a>) class="#{klass}">#{project_ref}$#{id}</a>)
else else
match match
end end
end end
end end
def project
context[:project]
end
def url_for_snippet(snippet, project) def url_for_snippet(snippet, project)
h = Rails.application.routes.url_helpers h = Rails.application.routes.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet, h.namespace_project_snippet_url(project.namespace, project, snippet,
......
...@@ -51,7 +51,7 @@ module Gitlab::Markdown ...@@ -51,7 +51,7 @@ module Gitlab::Markdown
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end end
it 'ignores invalid issue IDs' do it 'ignores invalid commit IDs' do
exp = act = "See #{commit1.id.reverse}...#{commit2.id}" exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
expect(project).to receive(:valid_repo?).and_return(true) expect(project).to receive(:valid_repo?).and_return(true)
...@@ -83,31 +83,32 @@ module Gitlab::Markdown ...@@ -83,31 +83,32 @@ module Gitlab::Markdown
end end
end end
# TODO (rspeicher): Remove or re-enable 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, namespace: namespace) }
# let(:project2) { create(:project, namespace: namespace) } let(:commit1) { project.repository.commit }
# let(:commit1) { project.repository.commit } let(:commit2) { project.repository.commit("HEAD~2") }
# let(:commit2) { project.repository.commit("HEAD~2") } let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
# let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
# it 'links to a valid reference' do it 'links to a valid reference' do
# doc = filter("See #{reference}") doc = filter("See #{reference}")
# expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
# to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id)
# end end
# it 'links with adjacent text' do it 'links with adjacent text' do
# doc = filter("Fixed (#{reference}.)") doc = filter("Fixed (#{reference}.)")
# expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
# end end
# it 'ignores invalid issue IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
# exp = act = "Fixed #{project2.path_with_namespace}##{commit.id.reverse}" exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}"
expect(filter(act).to_html).to eq exp
# expect(filter(act).to_html).to eq exp exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}"
# end expect(filter(act).to_html).to eq exp
# end end
end
end end
end end
...@@ -38,7 +38,7 @@ module Gitlab::Markdown ...@@ -38,7 +38,7 @@ module Gitlab::Markdown
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end end
it 'ignores invalid issue IDs' do it 'ignores invalid commit IDs' do
exp = act = "See #{reference.reverse}" exp = act = "See #{reference.reverse}"
expect(project).to receive(:valid_repo?).and_return(true) expect(project).to receive(:valid_repo?).and_return(true)
...@@ -88,9 +88,8 @@ module Gitlab::Markdown ...@@ -88,9 +88,8 @@ module Gitlab::Markdown
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end end
it 'ignores invalid issue IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Fixed #{project2.path_with_namespace}##{commit.id.reverse}" exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
......
...@@ -20,45 +20,72 @@ module Gitlab::Markdown ...@@ -20,45 +20,72 @@ module Gitlab::Markdown
end end
end end
it 'links to a valid reference' do context 'internal reference' do
doc = filter("See #{reference}") it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls. expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_snippet_url(project.namespace, project, snippet) namespace_project_snippet_url(project.namespace, project, snippet)
end end
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Snippet (#{reference}.)") doc = filter("Snippet (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end end
it 'ignores invalid snippet IDs' do it 'ignores invalid snippet IDs' do
exp = act = "Snippet $#{snippet.id + 1}" exp = act = "Snippet $#{snippet.id + 1}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
it 'includes a title attribute' do it 'includes a title attribute' do
doc = filter("Snippet #{reference}") doc = filter("Snippet #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
end end
it 'includes default classes' do it 'includes default classes' do
doc = filter("Snippet #{reference}") doc = filter("Snippet #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
end end
it 'includes an optional custom class' do it 'includes an optional custom class' do
doc = filter("Snippet #{reference}", reference_class: 'custom') doc = filter("Snippet #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom' expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Snippet #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end
end end
it 'supports an :only_path context' do context 'cross-project reference' do
doc = filter("Snippet #{reference}", only_path: true) let(:namespace) { create(:namespace, name: 'cross-reference') }
link = doc.css('a').first.attr('href') let(:project2) { create(:empty_project, namespace: namespace) }
let(:snippet) { create(:project_snippet, project: project2) }
let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
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
expect(link).not_to match %r(https?://) it 'links with adjacent text' do
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) doc = filter("See (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid snippet IDs on the referenced project' do
exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}"
expect(filter(act).to_html).to eq exp
end
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