From d6a5b45c8ea5ec7a68e213636fde405c52bb90e4 Mon Sep 17 00:00:00 2001
From: Douwe Maan <douwe@gitlab.com>
Date: Mon, 30 Nov 2015 21:14:46 +0100
Subject: [PATCH] Recognize issue/MR/snippet/commit links as references.

---
 app/models/commit.rb                          | 15 ++--
 app/models/commit_range.rb                    | 11 ++-
 app/models/concerns/referable.rb              | 19 +++++
 app/models/issue.rb                           | 11 ++-
 app/models/merge_request.rb                   | 11 ++-
 app/models/snippet.rb                         | 11 ++-
 lib/gitlab/markdown.rb                        |  5 +-
 .../markdown/abstract_reference_filter.rb     | 31 +++++---
 .../markdown/commit_range_reference_filter.rb | 70 +++++--------------
 .../markdown/commit_reference_filter.rb       | 68 +++++-------------
 lib/gitlab/markdown/external_link_filter.rb   |  3 +
 .../merge_request_reference_filter.rb         | 10 +++
 lib/gitlab/markdown/redactor_filter.rb        |  5 +-
 .../commit_range_reference_filter_spec.rb     | 39 ++++++++++-
 .../markdown/commit_reference_filter_spec.rb  | 31 ++++++++
 .../markdown/issue_reference_filter_spec.rb   | 32 +++++++++
 .../merge_request_reference_filter_spec.rb    | 29 +++++++-
 .../markdown/snippet_reference_filter_spec.rb | 30 ++++++++
 18 files changed, 301 insertions(+), 130 deletions(-)

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})?
-      (?<commit>\h{6,40})
+      #{link_reference_pattern} |
+      (?:
+        (?:#{Project.reference_pattern}#{reference_prefix})?
+        (?<commit>\h{6,40})
+      )
     }x
   end
 
+  def self.link_reference_pattern
+    super("commit", /(?<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})?
-      (?<commit_range>#{PATTERN})
+      #{link_reference_pattern} |
+      (?:
+        (?:#{Project.reference_pattern}#{reference_prefix})?
+        (?<commit_range>#{PATTERN})
+      )
     }x
   end
 
+  def self.link_reference_pattern
+    super("compare", /(?<commit_range>#{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{
+        (?<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
 
   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)}(?<issue>\d+)
+      #{link_reference_pattern} |
+      (?:
+        (#{Project.reference_pattern})?
+        #{Regexp.escape(reference_prefix)}(?<issue>\d+)
+      )
     }x
   end
 
+  def self.link_reference_pattern
+    super("issues", /(?<issue>\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)}(?<merge_request>\d+)
+      #{link_reference_pattern} |
+      (?:
+        (#{Project.reference_pattern})?
+        #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
+      )
     }x
   end
 
+  def self.link_reference_pattern
+    super("merge_requests", /(?<merge_request>\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)}(?<snippet>\d+)
+      #{link_reference_pattern} |
+      (?:
+        (#{Project.reference_pattern})?
+        #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
+      )
     }x
   end
 
+  def self.link_reference_pattern
+    super("snippets", /(?<snippet>\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?
 
             %(<a href="#{url}" #{data}
                  title="#{title}"
-                 class="#{klass}">#{match}</a>)
+                 class="#{klass}">#{text}</a>)
           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|
-      #     "<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)
         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
-      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)
+      def self.find_object(project, id)
+        range = CommitRange.new(id, project)
 
-            project_ref += '@' if project_ref
+        range.valid_commits? ? range : nil
+      end
 
-            %(<a href="#{url}" #{data}
-                 title="#{title}"
-                 class="#{klass}">#{project_ref}#{range}</a>)
-          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|
-      #     "<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)
         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
-
-            %(<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)
+      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(/\(<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>\.\)/)
       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(/\(<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
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(/\(<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
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(/\(<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
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(/\(<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
-- 
GitLab