From b88da58cb6272a86b6df2e4efe392f10e689a6b2 Mon Sep 17 00:00:00 2001
From: Robert Speicher <rspeicher@gmail.com>
Date: Thu, 14 May 2015 16:59:39 -0400
Subject: [PATCH] Add `reference_pattern` to Referable models

---
 app/models/commit.rb                           | 13 +++++++++++++
 app/models/commit_range.rb                     | 18 ++++++++++++++++--
 app/models/concerns/referable.rb               | 10 ++++++++++
 app/models/external_issue.rb                   | 13 +++++++++----
 app/models/group.rb                            |  6 +++++-
 app/models/issue.rb                            | 14 ++++++++++++--
 app/models/label.rb                            | 16 ++++++++++++++++
 app/models/merge_request.rb                    | 10 ++++++++++
 app/models/project.rb                          |  5 +++++
 app/models/snippet.rb                          | 10 ++++++++++
 app/models/user.rb                             |  8 ++++++++
 .../markdown/commit_range_reference_filter.rb  |  9 ++-------
 lib/gitlab/markdown/commit_reference_filter.rb | 11 ++---------
 lib/gitlab/markdown/cross_project_reference.rb |  3 ---
 .../external_issue_reference_filter.rb         |  7 ++-----
 lib/gitlab/markdown/issue_reference_filter.rb  |  9 ++-------
 lib/gitlab/markdown/label_reference_filter.rb  | 17 ++---------------
 .../markdown/merge_request_reference_filter.rb |  9 ++-------
 .../markdown/snippet_reference_filter.rb       |  9 ++-------
 lib/gitlab/markdown/user_reference_filter.rb   |  7 ++-----
 20 files changed, 130 insertions(+), 74 deletions(-)

diff --git a/app/models/commit.rb b/app/models/commit.rb
index 085f4e6398f..2c244fc0410 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -62,6 +62,19 @@ class Commit
     (self.class === other) && (raw == other.raw)
   end
 
+  def self.reference_prefix
+    '@'
+  end
+
+  # Pattern used to extract commit references from text
+  #
+  # The SHA can be between 6 and 40 hex characters.
+  #
+  # This pattern supports cross-project references.
+  def self.reference_pattern
+    %r{(?:#{Project.reference_pattern}#{reference_prefix})?(?<commit>\h{6,40})}
+  end
+
   def to_reference(from_project = nil)
     if cross_project_reference?(from_project)
       "#{project.to_reference}@#{id}"
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index fb1f6d09be6..86fc9eb01a3 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -29,10 +29,24 @@ class CommitRange
   # See `exclude_start?`
   attr_reader :exclude_start
 
-  # The beginning and ending SHA sums can be between 6 and 40 hex characters,
-  # and the range selection can be double- or triple-dot.
+  # The beginning and ending SHAs can be between 6 and 40 hex characters, and
+  # the range notation can be double- or triple-dot.
   PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
 
+  def self.reference_prefix
+    '@'
+  end
+
+  # Pattern used to extract commit range references from text
+  #
+  # This pattern supports cross-project references.
+  def self.reference_pattern
+    %r{
+      (?:#{Project.reference_pattern}#{reference_prefix})?
+      (?<commit_range>#{PATTERN})
+    }x
+  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 b41df301c3f..e3c1c6d268e 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -35,6 +35,16 @@ module Referable
     def reference_prefix
       ''
     end
+
+    # Regexp pattern used to match references to this object
+    #
+    # This must be overridden by the including class.
+    #
+    # Returns Regexp
+    def reference_pattern
+      raise NotImplementedError,
+        %Q{#{self} does not implement "reference_pattern"}
+    end
   end
 
   private
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 6fda4a2ab77..49f6c95e045 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -9,10 +9,6 @@ class ExternalIssue
     @issue_identifier.to_s
   end
 
-  def to_reference(_from_project = nil)
-    id
-  end
-
   def id
     @issue_identifier.to_s
   end
@@ -32,4 +28,13 @@ class ExternalIssue
   def project
     @project
   end
+
+  # Pattern used to extract `JIRA-123` issue references from text
+  def self.reference_pattern
+    %r{(?<issue>([A-Z\-]+-)\d+)}
+  end
+
+  def to_reference(_from_project = nil)
+    id
+  end
 end
diff --git a/app/models/group.rb b/app/models/group.rb
index 33d72e0d9ee..b4e908c5602 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -40,7 +40,11 @@ class Group < Namespace
     end
 
     def reference_prefix
-      '@'
+      User.reference_prefix
+    end
+
+    def reference_pattern
+      User.reference_pattern
     end
   end
 
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 31803b57b3f..ea6b9329b07 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -50,12 +50,22 @@ class Issue < ActiveRecord::Base
     state :closed
   end
 
+  def hook_attrs
+    attributes
+  end
+
   def self.reference_prefix
     '#'
   end
 
-  def hook_attrs
-    attributes
+  # Pattern used to extract `#123` issue references from text
+  #
+  # This pattern supports cross-project references.
+  def self.reference_pattern
+    %r{
+      #{Project.reference_pattern}?
+      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
+    }x
   end
 
   def to_reference(from_project = nil)
diff --git a/app/models/label.rb b/app/models/label.rb
index 013e6bf5978..8980049cef8 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -40,6 +40,22 @@ class Label < ActiveRecord::Base
     '~'
   end
 
+  # Pattern used to extract label references from text
+  #
+  # TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
+  def self.reference_pattern
+    %r{
+      #{reference_prefix}
+      (?:
+        (?<label_id>\d+)   | # Integer-based label ID, or
+        (?<label_name>
+          [A-Za-z0-9_-]+   | # String-based single-word label title
+          ['"][^&\?,]+['"]   # String-based multi-word label surrounded in quotes
+        )
+      )
+    }x
+  end
+
   # Returns the String necessary to reference this Label in Markdown
   #
   # format - Symbol format to use (default: :id, optional: :name)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 60b0ce6c018..6c90d09b866 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -140,6 +140,16 @@ class MergeRequest < ActiveRecord::Base
     '!'
   end
 
+  # Pattern used to extract `!123` merge request references from text
+  #
+  # This pattern supports cross-project references.
+  def self.reference_pattern
+    %r{
+      #{Project.reference_pattern}?
+      #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
+    }x
+  end
+
   def to_reference(from_project = nil)
     reference = "#{self.class.reference_prefix}#{iid}"
 
diff --git a/app/models/project.rb b/app/models/project.rb
index c943114449a..3c9f0dad28b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -248,6 +248,11 @@ class Project < ActiveRecord::Base
         order_by(method)
       end
     end
+
+    def reference_pattern
+      name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
+      %r{(?<project>#{name_pattern}/#{name_pattern})}
+    end
   end
 
   def team
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 8c3167833aa..d1619071f49 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -56,6 +56,16 @@ class Snippet < ActiveRecord::Base
     '$'
   end
 
+  # Pattern used to extract `$123` snippet references from text
+  #
+  # This pattern supports cross-project references.
+  def self.reference_pattern
+    %r{
+      #{Project.reference_pattern}?
+      #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
+    }x
+  end
+
   def to_reference(from_project = nil)
     reference = "#{self.class.reference_prefix}#{id}"
 
diff --git a/app/models/user.rb b/app/models/user.rb
index f546dc015c2..50ca4bc5acc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -253,6 +253,14 @@ class User < ActiveRecord::Base
     def reference_prefix
       '@'
     end
+
+    # Pattern used to extract `@user` user references from text
+    def reference_pattern
+      %r{
+        #{Regexp.escape(reference_prefix)}
+        (?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})
+      }x
+    end
   end
 
   #
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index 8764f7e474f..61591a9914b 100644
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -19,7 +19,7 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(COMMIT_RANGE_PATTERN) do |match|
+        text.gsub(CommitRange.reference_pattern) do |match|
           yield match, $~[:commit_range], $~[:project]
         end
       end
@@ -30,13 +30,8 @@ module Gitlab
         @commit_map = {}
       end
 
-      # Pattern used to extract commit range references from text
-      #
-      # This pattern supports cross-project references.
-      COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>#{CommitRange::PATTERN})/
-
       def call
-        replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content|
+        replace_text_nodes_matching(CommitRange.reference_pattern) do |content|
           commit_range_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
index b20b29f5d0c..f6932e76e70 100644
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -19,20 +19,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(COMMIT_PATTERN) do |match|
+        text.gsub(Commit.reference_pattern) do |match|
           yield match, $~[:commit], $~[:project]
         end
       end
 
-      # Pattern used to extract commit references from text
-      #
-      # The SHA1 sum can be between 6 and 40 hex characters.
-      #
-      # This pattern supports cross-project references.
-      COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
-
       def call
-        replace_text_nodes_matching(COMMIT_PATTERN) do |content|
+        replace_text_nodes_matching(Commit.reference_pattern) do |content|
           commit_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
index c436fabd658..66c256c5104 100644
--- a/lib/gitlab/markdown/cross_project_reference.rb
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -3,9 +3,6 @@ module Gitlab
     # Common methods for ReferenceFilters that support an optional cross-project
     # reference.
     module CrossProjectReference
-      NAMING_PATTERN  = Gitlab::Regex::NAMESPACE_REGEX_STR
-      PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
-
       # Given a cross-project reference string, get the Project record
       #
       # Defaults to value of `context[:project]` if:
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
index 0fc3f4cca06..2e74c6e45e2 100644
--- a/lib/gitlab/markdown/external_issue_reference_filter.rb
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -16,19 +16,16 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(ISSUE_PATTERN) do |match|
+        text.gsub(ExternalIssue.reference_pattern) do |match|
           yield match, $~[:issue]
         end
       end
 
-      # Pattern used to extract `JIRA-123` issue references from text
-      ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
-
       def call
         # Early return if the project isn't using an external tracker
         return doc if project.nil? || project.default_issues_tracker?
 
-        replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+        replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
           issue_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
index 1e885615163..2815626e247 100644
--- a/lib/gitlab/markdown/issue_reference_filter.rb
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -20,18 +20,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(ISSUE_PATTERN) do |match|
+        text.gsub(Issue.reference_pattern) do |match|
           yield match, $~[:issue].to_i, $~[:project]
         end
       end
 
-      # Pattern used to extract `#123` issue references from text
-      #
-      # This pattern supports cross-project references.
-      ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
-
       def call
-        replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+        replace_text_nodes_matching(Issue.reference_pattern) do |content|
           issue_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
index 1a77becee89..9f8c85b7012 100644
--- a/lib/gitlab/markdown/label_reference_filter.rb
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -15,26 +15,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(LABEL_PATTERN) do |match|
+        text.gsub(Label.reference_pattern) do |match|
           yield match, $~[:label_id].to_i, $~[:label_name]
         end
       end
 
-      # Pattern used to extract label references from text
-      #
-      # TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
-      LABEL_PATTERN = %r{
-        ~(
-          (?<label_id>\d+)   | # Integer-based label ID, or
-          (?<label_name>
-            [A-Za-z0-9_-]+   | # String-based single-word label title
-            ['"][^&\?,]+['"]   # String-based multi-word label surrounded in quotes
-          )
-        )
-      }x
-
       def call
-        replace_text_nodes_matching(LABEL_PATTERN) do |content|
+        replace_text_nodes_matching(Label.reference_pattern) do |content|
           label_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
index 740d72abb36..fddc050635f 100644
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -20,18 +20,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(MERGE_REQUEST_PATTERN) do |match|
+        text.gsub(MergeRequest.reference_pattern) do |match|
           yield match, $~[:merge_request].to_i, $~[:project]
         end
       end
 
-      # Pattern used to extract `!123` merge request references from text
-      #
-      # This pattern supports cross-project references.
-      MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
-
       def call
-        replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content|
+        replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
           merge_request_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
index 64a0a2696f7..f22f08de27c 100644
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -20,18 +20,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(SNIPPET_PATTERN) do |match|
+        text.gsub(Snippet.reference_pattern) do |match|
           yield match, $~[:snippet].to_i, $~[:project]
         end
       end
 
-      # Pattern used to extract `$123` snippet references from text
-      #
-      # This pattern supports cross-project references.
-      SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
-
       def call
-        replace_text_nodes_matching(SNIPPET_PATTERN) do |content|
+        replace_text_nodes_matching(Snippet.reference_pattern) do |content|
           snippet_link_filter(content)
         end
       end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
index 28ec041b1d4..ca7fd7b0338 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -16,16 +16,13 @@ module Gitlab
       #
       # Returns a String replaced with the return of the block.
       def self.references_in(text)
-        text.gsub(USER_PATTERN) do |match|
+        text.gsub(User.reference_pattern) do |match|
           yield match, $~[:user]
         end
       end
 
-      # Pattern used to extract `@user` user references from text
-      USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
-
       def call
-        replace_text_nodes_matching(USER_PATTERN) do |content|
+        replace_text_nodes_matching(User.reference_pattern) do |content|
           user_link_filter(content)
         end
       end
-- 
GitLab