diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-05-27 10:03:14 +0000 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-05-27 10:03:14 +0000 |
commit | 701c2e9a3b4b60642eba0ee00c3e11e1e43fb131 (patch) | |
tree | c5bca23181bc9320cab36619b2287f1cc405623d | |
parent | 3ed05b2191a7ede4734e2b7952f3810e5a1ee728 (diff) | |
parent | 3cb6a338466ca9b8e2a831cce306fc6d650231ed (diff) | |
download | gitlab-ce-701c2e9a3b4b60642eba0ee00c3e11e1e43fb131.tar.gz |
Merge branch 'rs-to_reference' into 'master'
Add to_reference method to referable models
Now there is a single source of information for which attribute a model uses to be referenced, and its special character.
See merge request !641
54 files changed, 866 insertions, 472 deletions
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 7bcc011fd5f..d89f7b4a28d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -135,15 +135,25 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String def cross_project_reference(project, entity) - path = project.path_with_namespace - - if entity.kind_of?(Issue) - [path, entity.iid].join('#') - elsif entity.kind_of?(MergeRequest) - [path, entity.iid].join('!') + if entity.respond_to?(:to_reference) + "#{project.to_reference}#{entity.to_reference}" else - raise 'Not supported type' + '' end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index be5a118bfec..f02fe240540 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,9 +1,11 @@ class Commit - include ActiveModel::Conversion - include StaticModel extend ActiveModel::Naming + + include ActiveModel::Conversion include Mentionable include Participable + include Referable + include StaticModel attr_mentionable :safe_message participant :author, :committer, :notes, :mentioned_users @@ -56,6 +58,34 @@ class Commit @raw.id end + def ==(other) + (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}) + }x + end + + def to_reference(from_project = nil) + if cross_project_reference?(from_project) + "#{project.to_reference}@#{id}" + else + id + end + end + def diff_line_count @diff_line_count ||= Commit::diff_line_count(self.diffs) @diff_line_count @@ -126,11 +156,6 @@ class Commit Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end - # Mentionable override. - def gfm_reference - "commit #{id}" - end - def author User.find_for_commit(author_email, author_name) end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index e6456198264..86fc9eb01a3 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -19,6 +19,7 @@ # class CommitRange include ActiveModel::Conversion + include Referable attr_reader :sha_from, :notation, :sha_to @@ -28,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. @@ -59,6 +74,17 @@ class CommitRange "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" end + def to_reference(from_project = nil) + # Not using to_s because we want the full SHAs + reference = sha_from + notation + sha_to + + if cross_project_reference?(from_project) + reference = project.to_reference + '@' + reference + end + + reference + end + # Returns a String for use in a link's title attribute def reference_title "Commits #{suffixed_sha_from} through #{sha_to}" diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index b7c39df885d..6f9f54d08cc 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,10 +20,15 @@ module Mentionable end end - # Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must - # be overridden if this model object can be referenced directly by GFM notation. - def gfm_reference - raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") + # Returns the text used as the body of a Note when this object is referenced + # + # By default this will be the class name and the result of calling + # `to_reference` on the object. + def gfm_reference(from_project = nil) + # "MergeRequest" > "merge_request" > "Merge request" > "merge request" + friendly_name = self.class.to_s.underscore.humanize.downcase + + "#{friendly_name} #{to_reference(from_project)}" end # Construct a String that contains possible GFM references. diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb new file mode 100644 index 00000000000..cced66cc1e4 --- /dev/null +++ b/app/models/concerns/referable.rb @@ -0,0 +1,61 @@ +# == Referable concern +# +# Contains functionality related to making a model referable in Markdown, such +# as "#1", "!2", "~3", etc. +module Referable + extend ActiveSupport::Concern + + # Returns the String necessary to reference this object in Markdown + # + # from_project - Refering Project object + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.first.to_reference # => "#1" + # Issue.last.to_reference(other_project) # => "cross-project#1" + # + # Returns a String + def to_reference(_from_project = nil) + '' + end + + module ClassMethods + # The character that prefixes the actual reference identifier + # + # This should be overridden by the including class. + # + # Examples: + # + # Issue.reference_prefix # => '#' + # MergeRequest.reference_prefix # => '!' + # + # Returns a String + def reference_prefix + '' + end + + # Regexp pattern used to match references to this object + # + # This must be overridden by the including class. + # + # Returns a Regexp + def reference_pattern + raise NotImplementedError, "#{self} does not implement #{__method__}" + end + end + + private + + # Check if a reference is being done cross-project + # + # from_project - Refering Project object + def cross_project_reference?(from_project) + if self.is_a?(Project) + self != from_project + else + from_project && self.project && self.project != from_project + end + end +end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 85fdb12bfdc..49f6c95e045 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -1,4 +1,6 @@ class ExternalIssue + include Referable + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -26,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 687458adac4..b4e908c5602 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -17,6 +17,8 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Group < Namespace + include Referable + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :users, through: :group_members @@ -36,6 +38,18 @@ class Group < Namespace def sort(method) order_by(method) end + + def reference_prefix + User.reference_prefix + end + + def reference_pattern + User.reference_pattern + end + end + + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{name}" end def human_name diff --git a/app/models/issue.rb b/app/models/issue.rb index 6e102051387..2456b7d0dc1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -21,10 +21,11 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Issue < ActiveRecord::Base - include Issuable include InternalId - include Taskable + include Issuable + include Referable include Sortable + include Taskable ActsAsTaggableOn.strict_case_match = true @@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base attributes end - # Mentionable overrides. + def self.reference_prefix + '#' + end + + # 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) + reference = "#{self.class.reference_prefix}#{iid}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end - def gfm_reference - "issue ##{iid}" + reference end # Reset issue events cache diff --git a/app/models/label.rb b/app/models/label.rb index eee28acefc1..230631b5180 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -11,6 +11,8 @@ # class Label < ActiveRecord::Base + include Referable + DEFAULT_COLOR = '#428BCA' default_value_for :color, DEFAULT_COLOR @@ -34,6 +36,45 @@ class Label < ActiveRecord::Base alias_attribute :name, :title + def self.reference_prefix + '~' + end + + # Pattern used to extract label references from text + 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, or + "[^&\?,]+" # 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) + # + # Note that its argument differs from other objects implementing Referable. If + # a non-Symbol argument is given (such as a Project), it will default to :id. + # + # Examples: + # + # Label.first.to_reference # => "~1" + # Label.first.to_reference(:name) # => "~\"bug\"" + # + # Returns a String + def to_reference(format = :id) + if format == :name && !name.include?('"') + %(#{self.class.reference_prefix}"#{name}") + else + "#{self.class.reference_prefix}#{id}" + end + end + def open_issues_count issues.opened.count end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 64f3c39f131..c57016dd6a2 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -25,10 +25,11 @@ require Rails.root.join("app/models/commit") require Rails.root.join("lib/static_model") class MergeRequest < ActiveRecord::Base - include Issuable - include Taskable include InternalId + include Issuable + include Referable include Sortable + include Taskable belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" @@ -135,6 +136,30 @@ class MergeRequest < ActiveRecord::Base scope :closed, -> { with_states(:closed, :merged) } scope :declined, -> { with_states(:closed) } + def self.reference_prefix + '!' + 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}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -289,11 +314,6 @@ class MergeRequest < ActiveRecord::Base end end - # Mentionable override. - def gfm_reference - "merge request !#{iid}" - end - def target_project_path if target_project target_project.path_with_namespace diff --git a/app/models/note.rb b/app/models/note.rb index 6939a7e73a0..d5f716b3de0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -326,8 +326,8 @@ class Note < ActiveRecord::Base end # Mentionable override. - def gfm_reference - noteable.gfm_reference + def gfm_reference(from_project = nil) + noteable.gfm_reference(from_project) end # Mentionable override. diff --git a/app/models/project.rb b/app/models/project.rb index 09d3ffd22fe..3c9f0dad28b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,11 +33,12 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Project < ActiveRecord::Base - include Sortable + include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::ConfigHelper include Rails.application.routes.url_helpers + include Referable + include Sortable extend Gitlab::ConfigHelper extend Enumerize @@ -247,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 @@ -305,6 +311,10 @@ class Project < ActiveRecord::Base path end + def to_reference(_from_project = nil) + path_with_namespace + end + def web_url [gitlab_config.url, path_with_namespace].join('/') end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index d2af26539b6..3ab9e834c63 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -16,14 +16,16 @@ # class Snippet < ActiveRecord::Base - include Sortable - include Linguist::BlobHelper include Gitlab::VisibilityLevel + include Linguist::BlobHelper include Participable + include Referable + include Sortable default_value_for :visibility_level, Snippet::PRIVATE - belongs_to :author, class_name: "User" + belongs_to :author, class_name: 'User' + belongs_to :project has_many :notes, as: :noteable, dependent: :destroy @@ -50,6 +52,30 @@ class Snippet < ActiveRecord::Base participant :author, :notes + def self.reference_prefix + '$' + 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}" + + if cross_project_reference?(from_project) + reference = project.to_reference + reference + end + + reference + end + def self.content_types [ ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", diff --git a/app/models/user.rb b/app/models/user.rb index 4dd37e73564..50ca4bc5acc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -62,11 +62,13 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class User < ActiveRecord::Base - include Sortable - include Gitlab::ConfigHelper - include TokenAuthenticatable extend Gitlab::ConfigHelper + + include Gitlab::ConfigHelper include Gitlab::CurrentSettings + include Referable + include Sortable + include TokenAuthenticatable default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group @@ -247,6 +249,18 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end + + 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 # @@ -257,6 +271,10 @@ class User < ActiveRecord::Base username end + def to_reference(_from_project = nil) + "#{self.class.reference_prefix}#{username}" + end + def notification @notification ||= Notification.new(self) end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1909ae0d6f1..1527ae0486d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -10,7 +10,7 @@ class SystemNoteService # author - User performing the change # new_commits - Array of Commits added since last push # existing_commits - Array of Commits added in a previous push - # oldrev - TODO (rspeicher): I have no idea what this actually does + # oldrev - Optional String SHA of a previous Commit # # See new_commit_summary and existing_commit_summary. # @@ -157,11 +157,11 @@ class SystemNoteService # # Example Note text: # - # "Mentioned in #1" + # "mentioned in #1" # - # "Mentioned in !2" + # "mentioned in !2" # - # "Mentioned in 54f7727c" + # "mentioned in 54f7727c" # # See cross_reference_note_content. # @@ -169,7 +169,7 @@ class SystemNoteService def self.cross_reference(noteable, mentioner, author) return if cross_reference_disallowed?(noteable, mentioner) - gfm_reference = mentioner_gfm_ref(noteable, mentioner) + gfm_reference = mentioner.gfm_reference(noteable.project) note_options = { project: noteable.project, @@ -200,12 +200,21 @@ class SystemNoteService # # Returns Boolean def self.cross_reference_disallowed?(noteable, mentioner) - return false unless MergeRequest === mentioner - return false unless Commit === noteable + return false unless mentioner.is_a?(MergeRequest) + return false unless noteable.is_a?(Commit) mentioner.commits.include?(noteable) end + # Check if a cross reference to a noteable from a mentioner already exists + # + # This method is used to prevent multiple notes being created for a mention + # when a issue is updated, for example. + # + # noteable - Noteable object being referenced + # mentioner - Mentionable object + # + # Returns Boolean def self.cross_reference_exists?(noteable, mentioner) # Initial scope should be system notes of this noteable type notes = Note.system.where(noteable_type: noteable.class) @@ -217,7 +226,7 @@ class SystemNoteService notes = notes.where(noteable_id: noteable.id) end - gfm_reference = mentioner_gfm_ref(noteable, mentioner, true) + gfm_reference = mentioner.gfm_reference(noteable.project) notes = notes.where(note: cross_reference_note_content(gfm_reference)) notes.count > 0 @@ -229,39 +238,6 @@ class SystemNoteService Note.create(args.merge(system: true)) end - # Prepend the mentioner's namespaced project path to the GFM reference for - # cross-project references. For same-project references, return the - # unmodified GFM reference. - def self.mentioner_gfm_ref(noteable, mentioner, cross_reference = false) - # FIXME (rspeicher): This was breaking things. - # if mentioner.is_a?(Commit) && cross_reference - # return mentioner.gfm_reference.sub('commit ', 'commit %') - # end - - full_gfm_reference(mentioner.project, noteable.project, mentioner) - end - - # Return the +mentioner+ GFM reference. If the mentioner and noteable - # projects are not the same, add the mentioning project's path to the - # returned value. - def self.full_gfm_reference(mentioning_project, noteable_project, mentioner) - if mentioning_project == noteable_project - mentioner.gfm_reference - else - if mentioner.is_a?(Commit) - mentioner.gfm_reference.sub( - /(commit )/, - "\\1#{mentioning_project.path_with_namespace}@" - ) - else - mentioner.gfm_reference.sub( - /(issue |merge request )/, - "\\1#{mentioning_project.path_with_namespace}" - ) - end - end - end - def self.cross_reference_note_prefix 'mentioned in ' end @@ -286,7 +262,7 @@ class SystemNoteService # # noteable - MergeRequest object # existing_commits - Array of existing Commit objects - # oldrev - Optional String SHA of ... TODO (rspeicher): I have no idea what this actually does. + # oldrev - Optional String SHA of a previous Commit # # Examples: # diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 9610f9ce414..2feeeecc48b 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -29,7 +29,7 @@ - else = f.submit 'Save', class: "btn-save btn" - - if @snippet.respond_to?(:project) + - if @snippet.project_id = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index ab184d95c05..aeec595782c 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -8,7 +8,7 @@ module Gitlab def closed_by_message(message) return [] if message.nil? - + closing_statements = message.scan(ISSUE_CLOSING_REGEX). map { |ref| ref[0] }.join(" ") 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..afd28dd8cf3 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 @@ -51,7 +48,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{issue}</a>) + class="#{klass}">#{match}</a>) end end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index 1e885615163..dea04761ead 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 @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}##{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 1a77becee89..e022ca69c91 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 @@ -85,8 +72,7 @@ module Gitlab # Returns a Hash. def label_params(id, name) if name - # TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding. - { name: name.tr('\'"', '') } + { name: name.tr('"', '') } else { id: id } end diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 740d72abb36..80779819485 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 @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}!#{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a4303d96bef..be4d26af0fc 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/string/output_safety' -require 'html/pipeline' +require 'html/pipeline/filter' module Gitlab module Markdown diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index 64a0a2696f7..174ba58af6c 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 @@ -57,7 +52,7 @@ module Gitlab %(<a href="#{url}" title="#{title}" - class="#{klass}">#{project_ref}$#{id}</a>) + class="#{klass}">#{match}</a>) else match end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 28ec041b1d4..c9972957182 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 @@ -68,7 +65,8 @@ module Gitlab url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@all</a>) + text = User.reference_prefix + 'all' + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def link_to_namespace(namespace) @@ -86,7 +84,8 @@ module Gitlab url = urls.group_url(group, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@#{group}</a>) + text = Group.reference_prefix + group + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def link_to_user(user, namespace) @@ -94,7 +93,8 @@ module Gitlab url = urls.user_url(user, only_path: context[:only_path]) - %(<a href="#{url}" class="#{link_class}">@#{user}</a>) + text = User.reference_prefix + user + %(<a href="#{url}" class="#{link_class}">#{text}</a>) end def user_can_reference_group?(group) diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 133beba7b98..16d1ca55f8d 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -11,7 +11,7 @@ describe "GitLab Flavored Markdown", feature: true do end before do - Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details") + Commit.any_instance.stub(title: "fix #{issue.to_reference}\n\nask #{fred.to_reference} for details") end let(:commit) { project.commit } @@ -25,25 +25,25 @@ describe "GitLab Flavored Markdown", feature: true do it "should render title in commits#index" do visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in commits#show" do visit namespace_project_commit_path(project.namespace, project, commit) - expect(page).to have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end it "should render title in repositories#branches" do visit namespace_project_branches_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -57,20 +57,20 @@ describe "GitLab Flavored Markdown", feature: true do author: @user, assignee: @user, project: project, - title: "fix ##{@other_issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{@other_issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render subject in issues#index" do visit namespace_project_issues_path(project.namespace, project) - expect(page).to have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render subject in issues#show" do visit namespace_project_issue_path(project.namespace, project, @issue) - expect(page).to have_link("##{@other_issue.iid}") + expect(page).to have_link(@other_issue.to_reference) end it "should render details in issues#show" do @@ -83,19 +83,19 @@ describe "GitLab Flavored Markdown", feature: true do describe "for merge requests" do before do - @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix ##{issue.iid}") + @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}") end it "should render title in merge_requests#index" do visit namespace_project_merge_requests_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in merge_requests#show" do visit namespace_project_merge_request_path(project.namespace, project, @merge_request) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end end @@ -104,26 +104,26 @@ describe "GitLab Flavored Markdown", feature: true do before do @milestone = create(:milestone, project: project, - title: "fix ##{issue.iid}", - description: "ask @#{fred.username} for details") + title: "fix #{issue.to_reference}", + description: "ask #{fred.to_reference} for details") end it "should render title in milestones#index" do visit namespace_project_milestones_path(project.namespace, project) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render title in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) - expect(page).to have_link("##{issue.iid}") + expect(page).to have_link(issue.to_reference) end it "should render description in milestones#show" do visit namespace_project_milestone_path(project.namespace, project, @milestone) - expect(page).to have_link("@#{fred.username}") + expect(page).to have_link(fred.to_reference) end end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 8f3dfc8d5a9..d6954174660 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -66,6 +66,10 @@ describe 'GitLab Markdown' do @doc.at_css("##{id}").parent.next_element end + # Sometimes it can be useful to see the parsed output of the Markdown document + # for debugging. Uncomment this block to write the output to + # tmp/capybara/markdown_spec.html. + # # it 'writes to a file' do # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| # file.puts @md @@ -344,13 +348,13 @@ class MarkdownFeature end def commit - @commit ||= project.repository.commit + @commit ||= project.commit end def commit_range unless @commit_range - commit2 = project.repository.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}") + commit2 = project.commit('HEAD~3') + @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) end @commit_range @@ -376,11 +380,6 @@ class MarkdownFeature @xproject end - # Shortcut to "cross-reference/project" - def xref - xproject.path_with_namespace - end - def xissue @xissue ||= create(:issue, project: xproject) end @@ -394,13 +393,13 @@ class MarkdownFeature end def xcommit - @xcommit ||= xproject.repository.commit + @xcommit ||= xproject.commit end def xcommit_range unless @xcommit_range - xcommit2 = xproject.repository.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}") + xcommit2 = xproject.commit('HEAD~2') + @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) end @xcommit_range diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 64817ec6700..26fc4e38e5a 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -127,61 +127,61 @@ But it shouldn't autolink text inside certain tags: - <a>http://about.gitlab.com/</a> - <kbd>http://about.gitlab.com/</kbd> -### Reference Filters (e.g., #<%= issue.iid %>) +### Reference Filters (e.g., <%= issue.to_reference %>) -References should be parseable even inside _!<%= merge_request.iid %>_ emphasis. +References should be parseable even inside _<%= merge_request.to_reference %>_ emphasis. #### UserReferenceFilter - All: @all -- User: @<%= user.username %> -- Group: @<%= group.name %> -- Ignores invalid: @fake_user -- Ignored in code: `@<%= user.username %>` -- Ignored in links: [Link to @<%= user.username %>](#user-link) +- User: <%= user.to_reference %> +- Group: <%= group.to_reference %> +- Ignores invalid: <%= User.reference_prefix %>fake_user +- Ignored in code: `<%= user.to_reference %>` +- Ignored in links: [Link to <%= user.to_reference %>](#user-link) #### IssueReferenceFilter -- Issue: #<%= issue.iid %> -- Issue in another project: <%= xref %>#<%= xissue.iid %> -- Ignored in code: `#<%= issue.iid %>` -- Ignored in links: [Link to #<%= issue.iid %>](#issue-link) +- Issue: <%= issue.to_reference %> +- Issue in another project: <%= xissue.to_reference(project) %> +- Ignored in code: `<%= issue.to_reference %>` +- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) #### MergeRequestReferenceFilter -- Merge request: !<%= merge_request.iid %> -- Merge request in another project: <%= xref %>!<%= xmerge_request.iid %> -- Ignored in code: `!<%= merge_request.iid %>` -- Ignored in links: [Link to !<%= merge_request.iid %>](#merge-request-link) +- Merge request: <%= merge_request.to_reference %> +- Merge request in another project: <%= xmerge_request.to_reference(project) %> +- Ignored in code: `<%= merge_request.to_reference %>` +- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link) #### SnippetReferenceFilter -- Snippet: $<%= snippet.id %> -- Snippet in another project: <%= xref %>$<%= xsnippet.id %> -- Ignored in code: `$<%= snippet.id %>` -- Ignored in links: [Link to $<%= snippet.id %>](#snippet-link) +- Snippet: <%= snippet.to_reference %> +- Snippet in another project: <%= xsnippet.to_reference(project) %> +- Ignored in code: `<%= snippet.to_reference %>` +- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) #### CommitRangeReferenceFilter -- Range: <%= commit_range %> -- Range in another project: <%= xref %>@<%= xcommit_range %> -- Ignored in code: `<%= commit_range %>` -- Ignored in links: [Link to <%= commit_range %>](#commit-range-link) +- Range: <%= commit_range.to_reference %> +- Range in another project: <%= xcommit_range.to_reference(project) %> +- Ignored in code: `<%= commit_range.to_reference %>` +- Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link) #### CommitReferenceFilter -- Commit: <%= commit.id %> -- Commit in another project: <%= xref %>@<%= xcommit.id %> -- Ignored in code: `<%= commit.id %>` -- Ignored in links: [Link to <%= commit.id %>](#commit-link) +- Commit: <%= commit.to_reference %> +- Commit in another project: <%= xcommit.to_reference(project) %> +- Ignored in code: `<%= commit.to_reference %>` +- Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) #### LabelReferenceFilter -- Label by ID: ~<%= simple_label.id %> -- Label by name: ~<%= simple_label.name %> -- Label by name in quotes: ~"<%= label.name %>" -- Ignored in code: `~<%= simple_label.name %>` -- Ignored in links: [Link to ~<%= simple_label.id %>](#label-link) +- Label by ID: <%= simple_label.to_reference %> +- Label by name: <%= Label.reference_prefix %><%= simple_label.name %> +- Label by name in quotes: <%= label.to_reference(:name) %> +- Ignored in code: `<%= simple_label.to_reference %>` +- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) ### Task Lists diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 0d0418f84a7..d0b200a9ff8 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -26,7 +26,7 @@ describe GitlabMarkdownHelper do end describe "referencing multiple objects" do - let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } + let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" } it "should link to the merge request" do expected = namespace_project_merge_request_path(project.namespace, project, merge_request) @@ -50,7 +50,7 @@ describe GitlabMarkdownHelper do let(:issues) { create_list(:issue, 2, project: project) } it 'should handle references nested in links with all the text' do - actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) + actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) doc = Nokogiri::HTML.parse(actual) # Make sure we didn't create invalid markup @@ -63,7 +63,7 @@ describe GitlabMarkdownHelper do # First issue link expect(doc.css('a')[1].attr('href')). to eq namespace_project_issue_path(project.namespace, project, issues[0]) - expect(doc.css('a')[1].text).to eq "##{issues[0].iid}" + expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link expect(doc.css('a')[2].attr('href')).to eq commit_path @@ -72,7 +72,7 @@ describe GitlabMarkdownHelper do # Second issue link expect(doc.css('a')[3].attr('href')). to eq namespace_project_issue_path(project.namespace, project, issues[1]) - expect(doc.css('a')[3].text).to eq "##{issues[1].iid}" + expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link expect(doc.css('a')[4].attr('href')).to eq commit_path @@ -90,7 +90,7 @@ describe GitlabMarkdownHelper do end it "escapes HTML passed in as the body" do - actual = "This is a <h1>test</h1> - see ##{issues[0].iid}" + actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" expect(link_to_gfm(actual, commit_path)). to match('<h1>test</h1>') end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index cb7b0fbb890..5d7ff4f6122 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -1,131 +1,131 @@ require 'spec_helper' describe Gitlab::ClosingIssueExtractor do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } - let(:iid1) { issue.iid } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:reference) { issue.to_reference } subject { described_class.new(project, project.creator) } describe "#closed_by_message" do context 'with a single reference' do it do - message = "Awesome commit (Closes ##{iid1})" + message = "Awesome commit (Closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (closes ##{iid1})" + message = "Awesome commit (closes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Closed ##{iid1}" + message = "Closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "closed ##{iid1}" + message = "closed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Closing ##{iid1}" + message = "Closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "closing ##{iid1}" + message = "closing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Close ##{iid1}" + message = "Close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "close ##{iid1}" + message = "close #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (Fixes ##{iid1})" + message = "Awesome commit (Fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (fixes ##{iid1})" + message = "Awesome commit (fixes #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fixed ##{iid1}" + message = "Fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fixed ##{iid1}" + message = "fixed #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fixing ##{iid1}" + message = "Fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fixing ##{iid1}" + message = "fixing #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Fix ##{iid1}" + message = "Fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "fix ##{iid1}" + message = "fix #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (Resolves ##{iid1})" + message = "Awesome commit (Resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Awesome commit (resolves ##{iid1})" + message = "Awesome commit (resolves #{reference})" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolved ##{iid1}" + message = "Resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolved ##{iid1}" + message = "resolved #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolving ##{iid1}" + message = "Resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolving ##{iid1}" + message = "resolving #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "Resolve ##{iid1}" + message = "Resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end it do - message = "resolve ##{iid1}" + message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end end @@ -133,40 +133,40 @@ describe Gitlab::ClosingIssueExtractor do context 'with multiple references' do let(:other_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) } - let(:iid2) { other_issue.iid } - let(:iid3) { third_issue.iid } + let(:reference2) { other_issue.to_reference } + let(:reference3) { third_issue.to_reference } it 'fetches issues in single line message' do - message = "Closes ##{iid1} and fix ##{iid2}" + message = "Closes #{reference} and fix #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do - message = "Closes ##{iid1}, closes ##{iid2}" + message = "Closes #{reference}, closes #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do - message = "Closes ##{iid1}, ##{iid2} and ##{iid3}" + message = "Closes #{reference}, #{reference2} and #{reference3}" expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do - message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}" + message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" expect(subject.closed_by_message(message)). to eq([issue, other_issue]) end it 'fetches issues in hybrid message' do - message = "Awesome commit (closes ##{iid1})\n"\ - "Also fixing issues ##{iid2}, ##{iid3} and #4" + message = "Awesome commit (closes #{reference})\n"\ + "Also fixing issues #{reference2}, #{reference3} and #4" expect(subject.closed_by_message(message)). to eq([issue, other_issue, third_issue]) 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 7274cb309a0..d3695ee46d0 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -8,34 +8,36 @@ module Gitlab::Markdown let(:commit1) { project.commit } let(:commit2) { project.commit("HEAD~2") } + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + it 'requires project context' do - expect { described_class.call('Commit Range 1c002d..d200c1', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>" + exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "#{commit1.id}...#{commit2.id}" } - let(:reference2) { "#{commit1.id}..#{commit2.id}" } + let(:reference) { range.to_reference } + let(:reference2) { range2.to_reference } it 'links to a valid two-dot reference' do doc = filter("See #{reference2}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id) + to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param) end it 'links to a valid three-dot reference' do doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id) + to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) end it 'links to a valid short ID' do @@ -51,7 +53,7 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("See (#{reference}.)") - exp = Regexp.escape("#{commit1.short_id}...#{commit2.short_id}") + exp = Regexp.escape(range.to_s) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end @@ -65,7 +67,7 @@ module Gitlab::Markdown it 'includes a title attribute' do doc = filter("See #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}" + expect(doc.css('a').first.attr('title')).to eq range.reference_title end it 'includes default classes' do @@ -95,9 +97,11 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } - let(:commit1) { project.commit } - let(:commit2) { project.commit("HEAD~2") } - let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } + let(:reference) { range.to_reference(project) } + + before do + range.project = project2 + end context 'when user can access reference' do before { allow_cross_reference! } @@ -106,21 +110,21 @@ module Gitlab::Markdown doc = filter("See #{reference}") expect(doc.css('a').first.attr('href')). - to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id) + 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("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}" + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" expect(filter(act).to_html).to eq exp - exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index cc32a4fcf03..a0d2cd7e22b 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -8,8 +8,7 @@ module Gitlab::Markdown let(:commit) { project.commit } it 'requires project context' do - expect { described_class.call('Commit 1c002d', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -47,10 +46,11 @@ module Gitlab::Markdown end it 'ignores invalid commit IDs' do - exp = act = "See #{reference.reverse}" + invalid = invalidate_reference(reference) + exp = act = "See #{invalid}" expect(project).to receive(:valid_repo?).and_return(true) - expect(project.repository).to receive(:commit).with(reference.reverse) + expect(project.repository).to receive(:commit).with(invalid) expect(filter(act).to_html).to eq exp end @@ -93,8 +93,8 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } - let(:commit) { project.commit } - let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" } + let(:commit) { project2.commit } + let(:reference) { commit.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -109,12 +109,12 @@ module Gitlab::Markdown it 'links with adjacent text' do doc = filter("Fixed (#{reference}.)") - exp = Regexp.escape(project2.path_with_namespace) + exp = Regexp.escape(project2.to_reference) expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" + exp = act = "Committed #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb index b19bc125b92..bf9409589fa 100644 --- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -9,19 +9,18 @@ module Gitlab::Markdown end let(:project) { create(:jira_project) } - let(:issue) { double('issue', iid: 123) } context 'JIRA issue references' do - let(:reference) { "JIRA-#{issue.iid}" } + let(:issue) { ExternalIssue.new('JIRA-123', project) } + let(:reference) { issue.to_reference } it 'requires project context' do - expect { described_class.call('Issue JIRA-123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>" + exp = act = "<#{elem}>Issue #{reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end @@ -33,13 +32,6 @@ module Gitlab::Markdown expect(filter(act).to_html).to eq exp end - %w(pre code a style).each do |elem| - it "ignores references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end - it 'links to a valid reference' do doc = filter("Issue #{reference}") expect(doc.css('a').first.attr('href')) diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 08382b3e7e8..a838d7570c8 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -12,24 +12,23 @@ module Gitlab::Markdown let(:issue) { create(:issue, project: project) } it 'requires project context' do - expect { described_class.call('Issue #123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>" + exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "##{issue.iid}" } + let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) - exp = act = "Issue ##{issue.iid}" + exp = act = "Issue #{reference}" expect(filter(act).to_html).to eq exp end @@ -46,9 +45,9 @@ module Gitlab::Markdown end it 'ignores invalid issue IDs' do - exp = act = "Fixed ##{issue.iid + 1}" + invalid = invalidate_reference(reference) + exp = act = "Fixed #{invalid}" - expect(project).to receive(:get_issue).with(issue.iid + 1).and_return(nil) expect(filter(act).to_html).to eq exp end @@ -92,7 +91,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + let(:reference) { issue.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -101,7 +100,7 @@ module Gitlab::Markdown expect_any_instance_of(Project).to receive(:get_issue). with(issue.iid).and_return(nil) - exp = act = "Issue ##{issue.iid}" + exp = act = "Issue #{reference}" expect(filter(act).to_html).to eq exp end @@ -118,7 +117,7 @@ module Gitlab::Markdown end it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}" + exp = act = "Fixed #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index c4548e7431f..41987f57bca 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -7,11 +7,10 @@ module Gitlab::Markdown let(:project) { create(:empty_project) } let(:label) { create(:label, project: project) } - let(:reference) { "~#{label.id}" } + let(:reference) { label.to_reference } it 'requires project context' do - expect { described_class.call('Label ~123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -36,7 +35,7 @@ module Gitlab::Markdown link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) - expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true) + expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end it 'adds to the results hash' do @@ -70,7 +69,7 @@ module Gitlab::Markdown end it 'ignores invalid label IDs' do - exp = act = "Label ~#{label.id + 1}" + exp = act = "Label #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -78,7 +77,7 @@ module Gitlab::Markdown context 'String-based single-word references' do let(:label) { create(:label, name: 'gfm', project: project) } - let(:reference) { "~#{label.name}" } + let(:reference) { "#{Label.reference_prefix}#{label.name}" } it 'links to a valid reference' do doc = filter("See #{reference}") @@ -94,59 +93,33 @@ module Gitlab::Markdown end it 'ignores invalid label names' do - exp = act = "Label ~#{label.name.reverse}" + exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}" expect(filter(act).to_html).to eq exp end end context 'String-based multi-word references in quotes' do - let(:label) { create(:label, name: 'gfm references', project: project) } + let(:label) { create(:label, name: 'gfm references', project: project) } + let(:reference) { label.to_reference(:name) } - context 'in single quotes' do - let(:reference) { "~'#{label.name}'" } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end - - it 'ignores invalid label names' do - exp = act = "Label ~'#{label.name.reverse}'" + it 'links to a valid reference' do + doc = filter("See #{reference}") - expect(filter(act).to_html).to eq exp - end + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' end - context 'in double quotes' do - let(:reference) { %(~"#{label.name}") } - - it 'links to a valid reference' do - doc = filter("See #{reference}") - - expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_url(project.namespace, project, label_name: label.name) - expect(doc.text).to eq 'See gfm references' - end - - it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") - expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) - end + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end - it 'ignores invalid label names' do - exp = act = %(Label ~"#{label.name.reverse}") + it 'ignores invalid label names' do + exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}") - expect(filter(act).to_html).to eq exp - end + expect(filter(act).to_html).to eq exp 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 d6e745114f2..6aeb1093602 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -8,19 +8,18 @@ module Gitlab::Markdown let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do - expect { described_class.call('MergeRequest !123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>" + exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'internal reference' do - let(:reference) { "!#{merge.iid}" } + let(:reference) { merge.to_reference } it 'links to a valid reference' do doc = filter("See #{reference}") @@ -35,7 +34,7 @@ module Gitlab::Markdown end it 'ignores invalid merge IDs' do - exp = act = "Merge !#{merge.iid + 1}" + exp = act = "Merge #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -80,7 +79,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, namespace: namespace) } let(:merge) { create(:merge_request, source_project: project2) } - let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + let(:reference) { merge.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -99,7 +98,7 @@ module Gitlab::Markdown end it 'ignores invalid merge IDs on the referenced project' do - exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}" + exp = act = "Merge #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index a4b331157af..07ece66e903 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -6,11 +6,10 @@ module Gitlab::Markdown let(:project) { create(:empty_project) } let(:snippet) { create(:project_snippet, project: project) } - let(:reference) { "$#{snippet.id}" } + let(:reference) { snippet.to_reference } it 'requires project context' do - expect { described_class.call('Snippet $123', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end %w(pre code a style).each do |elem| @@ -34,7 +33,7 @@ module Gitlab::Markdown end it 'ignores invalid snippet IDs' do - exp = act = "Snippet $#{snippet.id + 1}" + exp = act = "Snippet #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end @@ -79,7 +78,7 @@ module Gitlab::Markdown let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, namespace: namespace) } let(:snippet) { create(:project_snippet, project: project2) } - let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + let(:reference) { snippet.to_reference(project) } context 'when user can access reference' do before { allow_cross_reference! } @@ -97,7 +96,7 @@ module Gitlab::Markdown end it 'ignores invalid snippet IDs on the referenced project' do - exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}" + exp = act = "See #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index 922502ada33..0ecbdee9b9e 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -4,65 +4,63 @@ module Gitlab::Markdown describe UserReferenceFilter do include ReferenceFilterSpecHelper - let(:project) { create(:empty_project) } - let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:reference) { user.to_reference } it 'requires project context' do - expect { described_class.call('Example @mention', {}) }. - to raise_error(ArgumentError, /:project/) + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end it 'ignores invalid users' do - exp = act = 'Hey @somebody' + exp = act = "Hey #{invalidate_reference(reference)}" expect(filter(act).to_html).to eq(exp) end %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do - exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>" + exp = act = "<#{elem}>Hey #{reference}</#{elem}>" expect(filter(act).to_html).to eq exp end end context 'mentioning @all' do + let(:reference) { User.reference_prefix + 'all' } + before do project.team << [project.creator, :developer] end it 'supports a special @all mention' do - doc = filter("Hey @all") + doc = filter("Hey #{reference}") expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) end it 'adds to the results hash' do - result = pipeline_result('Hey @all') + result = pipeline_result("Hey #{reference}") expect(result[:references][:user]).to eq [project.creator] end end context 'mentioning a user' do - let(:reference) { "@#{user.username}" } - it 'links to a User' do doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) end - # TODO (rspeicher): This test might be overkill it 'links to a User with a period' do user = create(:user, name: 'alphA.Beta') - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end - # TODO (rspeicher): This test might be overkill it 'links to a User with an underscore' do user = create(:user, name: 'ping_pong_king') - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end @@ -73,10 +71,9 @@ module Gitlab::Markdown end context 'mentioning a group' do - let(:group) { create(:group) } - let(:user) { create(:user) } - - let(:reference) { "@#{group.name}" } + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:reference) { group.to_reference } context 'that the current user can read' do before do @@ -108,23 +105,23 @@ module Gitlab::Markdown end it 'links with adjacent text' do - skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.' - doc = filter("Mention me (@#{user.username}.)") - expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) + skip "TODO (rspeicher): Re-enable when usernames can't end in periods." + doc = filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) end it 'includes default classes' do - doc = filter("Hey @#{user.username}") + doc = filter("Hey #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' end it 'includes an optional custom class' do - doc = filter("Hey @#{user.username}", reference_class: 'custom') + doc = filter("Hey #{reference}", reference_class: 'custom') expect(doc.css('a').first.attr('class')).to include 'custom' end it 'supports an :only_path context' do - doc = filter("Hey @#{user.username}", only_path: true) + doc = filter("Hey #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 9801dc16554..c14f4ac6bf6 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::ReferenceExtractor do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) - subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.") + subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") expect(subject.issues).to eq([@i0, @i1]) end @@ -82,7 +82,7 @@ describe Gitlab::ReferenceExtractor do end it 'handles project issue references' do - subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}") + subject.analyze("this refers issue #{issue.to_reference(project)}") extracted = subject.issues expect(extracted.size).to eq(1) expect(extracted).to eq([issue]) diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 31ee3e99cad..e7fb43ff335 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -1,6 +1,12 @@ require 'spec_helper' describe CommitRange do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + let(:sha_from) { 'f3f85602' } let(:sha_to) { 'e86e1013' } @@ -21,6 +27,23 @@ describe CommitRange do end end + describe '#to_reference' do + let(:project) { double('project', to_reference: 'namespace1/project') } + + before do + range.project = project + end + + it 'returns a String reference to the object' do + expect(range.to_reference).to eq range.to_s + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}" + end + end + describe '#reference_title' do it 'returns the correct String for three-dot ranges' do expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}" diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ad2ac143d97..27eb02a870b 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,8 +1,28 @@ require 'spec_helper' describe Commit do - let(:project) { create :project } - let(:commit) { project.commit } + let(:project) { create(:project) } + let(:commit) { project.commit } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Mentionable) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(StaticModel) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(commit.to_reference).to eq commit.id + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}" + end + end describe '#title' do it "returns no_commit_message when safe_message is blank" do diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb new file mode 100644 index 00000000000..7744610db78 --- /dev/null +++ b/spec/models/external_issue_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe ExternalIssue do + let(:project) { double('project', to_reference: 'namespace1/project1') } + let(:issue) { described_class.new('EXT-1234', project) } + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(issue.to_reference).to eq issue.id + end + end + + describe '#title' do + it 'returns a title' do + expect(issue.title).to eq "External Issue #{issue}" + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9428224a64f..80638fc8db2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -18,16 +18,30 @@ require 'spec_helper' describe Group do let!(:group) { create(:group) } - describe "Associations" do + describe 'associations' do it { is_expected.to have_many :projects } it { is_expected.to have_many :group_members } end - it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } - it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } - it { is_expected.not_to validate_presence_of :owner } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.not_to validate_presence_of :owner } + end + + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(group.to_reference).to eq "@#{group.name}" + end + end describe :users do it { expect(group.users).to eq(group.owners) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 20d823b40e5..614b648bb52 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -24,15 +24,30 @@ describe Issue do it { is_expected.to belong_to(:milestone) } end - describe "Mass assignment" do - end - describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(InternalId) } it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end subject { create(:issue) } + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "##{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)). + to eq "#{subject.project.to_reference}##{subject.iid}" + end + end + describe '#is_being_reassigned?' do it 'returns true if the issue assignee has changed' do subject.assignee = create(:user) @@ -45,11 +60,8 @@ describe Issue do describe '#is_being_reassigned?' do it 'returns issues assigned to user' do - user = create :user - - 2.times do - issue = create :issue, assignee: user - end + user = create(:user) + create_list(:issue, 2, assignee: user) expect(Issue.open_for(user).count).to eq 2 end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 8644ac46605..6518213d71c 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -14,30 +14,63 @@ require 'spec_helper' describe Label do let(:label) { create(:label) } - it { expect(label).to be_valid } - it { is_expected.to belong_to(:project) } + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:label_links).dependent(:destroy) } + it { is_expected.to have_many(:issues).through(:label_links).source(:target) } + end + + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Referable) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:project) } - describe 'Validation' do it 'should validate color code' do - expect(build(:label, color: 'G-ITLAB')).not_to be_valid - expect(build(:label, color: 'AABBCC')).not_to be_valid - expect(build(:label, color: '#AABBCCEE')).not_to be_valid - expect(build(:label, color: '#GGHHII')).not_to be_valid - expect(build(:label, color: '#')).not_to be_valid - expect(build(:label, color: '')).not_to be_valid - - expect(build(:label, color: '#AABBCC')).to be_valid + expect(label).not_to allow_value('G-ITLAB').for(:color) + expect(label).not_to allow_value('AABBCC').for(:color) + expect(label).not_to allow_value('#AABBCCEE').for(:color) + expect(label).not_to allow_value('GGHHII').for(:color) + expect(label).not_to allow_value('#').for(:color) + expect(label).not_to allow_value('').for(:color) + + expect(label).to allow_value('#AABBCC').for(:color) + expect(label).to allow_value('#abcdef').for(:color) end it 'should validate title' do - expect(build(:label, title: 'G,ITLAB')).not_to be_valid - expect(build(:label, title: 'G?ITLAB')).not_to be_valid - expect(build(:label, title: 'G&ITLAB')).not_to be_valid - expect(build(:label, title: '')).not_to be_valid + expect(label).not_to allow_value('G,ITLAB').for(:title) + expect(label).not_to allow_value('G?ITLAB').for(:title) + expect(label).not_to allow_value('G&ITLAB').for(:title) + expect(label).not_to allow_value('').for(:title) + + expect(label).to allow_value('GITLAB').for(:title) + expect(label).to allow_value('gitlab').for(:title) + expect(label).to allow_value("customer's request").for(:title) + end + end + + describe '#to_reference' do + context 'using id' do + it 'returns a String reference to the object' do + expect(label.to_reference).to eq "~#{label.id}" + expect(label.to_reference(double('project'))).to eq "~#{label.id}" + end + end + + context 'using name' do + it 'returns a String reference to the object' do + expect(label.to_reference(:name)).to eq %(~"#{label.name}") + end - expect(build(:label, title: 'GITLAB')).to be_valid - expect(build(:label, title: 'gitlab')).to be_valid + it 'uses id when name contains double quote' do + label = create(:label, name: %q{"irony"}) + expect(label.to_reference(:name)).to eq "~#{label.id}" + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 97b8abc49dd..0465aa34843 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -24,22 +24,45 @@ require 'spec_helper' describe MergeRequest do - describe "Validation" do - it { is_expected.to validate_presence_of(:target_branch) } - it { is_expected.to validate_presence_of(:source_branch) } + subject { create(:merge_request) } + + describe 'associations' do + it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } + it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } + + it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) } end - describe "Mass assignment" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(InternalId) } + it { is_expected.to include_module(Issuable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Taskable) } end - describe "Respond to" do + describe 'validation' do + it { is_expected.to validate_presence_of(:target_branch) } + it { is_expected.to validate_presence_of(:source_branch) } + end + + describe 'respond to' do it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) } end - describe 'modules' do - it { is_expected.to include_module(Issuable) } + describe '#to_reference' do + it 'returns a String reference to the object' do + expect(subject.to_reference).to eq "!#{subject.iid}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}" + end end describe "#mr_and_commit_notes" do @@ -57,8 +80,6 @@ describe MergeRequest do end end - subject { create(:merge_request) } - describe '#is_being_reassigned?' do it 'returns true if the merge_request assignee has changed' do subject.assignee = create(:user) @@ -108,7 +129,7 @@ describe MergeRequest do it 'detects issues mentioned in the description' do issue2 = create(:issue, project: subject.project) - subject.description = "Closes ##{issue2.iid}" + subject.description = "Closes #{issue2.to_reference}" subject.project.stub(default_branch: subject.target_branch) expect(subject.closes_issues).to include(issue2) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 37e21a90818..48568e2a3ff 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -32,7 +32,7 @@ require 'spec_helper' describe Project do - describe 'Associations' do + describe 'associations' do it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:creator).class_name('User') } @@ -54,10 +54,17 @@ describe Project do it { is_expected.to have_one(:asana_service).dependent(:destroy) } end - describe 'Mass assignment' do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::ShellAdapter) } + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe 'Validation' do + describe 'validation' do let!(:project) { create(:project) } it { is_expected.to validate_presence_of(:name) } @@ -91,6 +98,14 @@ describe Project do it { is_expected.to respond_to(:path_with_namespace) } end + describe '#to_reference' do + let(:project) { create(:empty_project) } + + it 'returns a String reference to the object' do + expect(project.to_reference).to eq project.path_with_namespace + end + end + it 'should return valid url to repo' do project = Project.new(path: 'somewhere') expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e37dcc75230..c81dd36ef4b 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -18,23 +18,47 @@ require 'spec_helper' describe Snippet do - describe "Associations" do - it { is_expected.to belong_to(:author).class_name('User') } - it { is_expected.to have_many(:notes).dependent(:destroy) } + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::VisibilityLevel) } + it { is_expected.to include_module(Linguist::BlobHelper) } + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } end - describe "Mass assignment" do + describe 'associations' do + it { is_expected.to belong_to(:author).class_name('User') } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:notes).dependent(:destroy) } end - describe "Validation" do + describe 'validation' do it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } it { is_expected.to ensure_length_of(:title).is_within(0..255) } it { is_expected.to validate_presence_of(:file_name) } - it { is_expected.to ensure_length_of(:title).is_within(0..255) } + it { is_expected.to ensure_length_of(:file_name).is_within(0..255) } it { is_expected.to validate_presence_of(:content) } + + it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) } + end + + describe '#to_reference' do + let(:project) { create(:empty_project) } + let(:snippet) { create(:snippet, project: project) } + + it 'returns a String reference to the object' do + expect(snippet.to_reference).to eq "$#{snippet.id}" + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0dddcd5bda2..e1205c18a85 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -63,7 +63,17 @@ require 'spec_helper' describe User do include Gitlab::CurrentSettings - describe "Associations" do + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Gitlab::ConfigHelper) } + it { is_expected.to include_module(Gitlab::CurrentSettings) } + it { is_expected.to include_module(Referable) } + it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(TokenAuthenticatable) } + end + + describe 'associations' do it { is_expected.to have_one(:namespace) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } @@ -79,9 +89,6 @@ describe User do it { is_expected.to have_many(:identities).dependent(:destroy) } end - describe "Mass assignment" do - end - describe 'validations' do it { is_expected.to validate_presence_of(:username) } it { is_expected.to validate_presence_of(:projects_limit) } @@ -175,6 +182,14 @@ describe User do it { is_expected.to respond_to(:private_token) } end + describe '#to_reference' do + let(:user) { create(:user) } + + it 'returns a String reference to the object' do + expect(user.to_reference).to eq "@#{user.username}" + end + end + describe '#generate_password' do it "should execute callback when force_random_password specified" do user = build(:user, force_random_password: true) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 6d8c71f94f9..0dcc94e8bd4 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -259,13 +259,13 @@ describe SystemNoteService do let(:mentioner) { project2.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{project2.path_with_namespace}@#{mentioner.id}" + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{project2.path_with_namespace}##{mentioner.iid}" + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" end end end @@ -275,13 +275,13 @@ describe SystemNoteService do let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.id}" + expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue ##{mentioner.iid}" + expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" end end end @@ -291,7 +291,7 @@ describe SystemNoteService do describe '.cross_reference?' do it 'is truthy when text begins with expected text' do - expect(described_class.cross_reference?('mentioned in issue #1')).to be_truthy + expect(described_class.cross_reference?('mentioned in something')).to be_truthy end it 'is falsey when text does not begin with expected text' do diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 53fb6545553..ede62e8f37a 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -10,12 +10,12 @@ def common_mentionable_setup let(:mentioned_issue) { create(:issue, project: project) } let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } - let(:mentioned_commit) { project.repository.commit } + let(:mentioned_commit) { project.commit } let(:ext_proj) { create(:project, :public) } let(:ext_issue) { create(:issue, project: ext_proj) } let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) } - let(:ext_commit) { ext_proj.repository.commit } + let(:ext_commit) { ext_proj.commit } # Override to add known commits to the repository stub. let(:extra_commits) { [] } @@ -23,21 +23,19 @@ def common_mentionable_setup # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference # to this string and place it in their +mentionable_text+. let(:ref_string) do - cross = ext_proj.path_with_namespace - <<-MSG.strip_heredoc These references are new: - Issue: ##{mentioned_issue.iid} - Merge: !#{mentioned_mr.iid} - Commit: #{mentioned_commit.id} + Issue: #{mentioned_issue.to_reference} + Merge: #{mentioned_mr.to_reference} + Commit: #{mentioned_commit.to_reference} This reference is a repeat and should only be mentioned once: - Repeat: ##{mentioned_issue.iid} + Repeat: #{mentioned_issue.to_reference} These references are cross-referenced: - Issue: #{cross}##{ext_issue.iid} - Merge: #{cross}!#{ext_mr.iid} - Commit: #{cross}@#{ext_commit.short_id} + Issue: #{ext_issue.to_reference(project)} + Merge: #{ext_mr.to_reference(project)} + Commit: #{ext_commit.to_reference(project)} This is a self-reference and should not be mentioned at all: Self: #{backref_text} @@ -109,19 +107,17 @@ shared_examples 'an editable mentionable' do it 'creates new cross-reference notes when the mentionable text is edited' do subject.save - cross = ext_proj.path_with_namespace - new_text = <<-MSG These references already existed: - Issue: ##{mentioned_issue.iid} - Commit: #{mentioned_commit.id} + Issue: #{mentioned_issue.to_reference} + Commit: #{mentioned_commit.to_reference} This cross-project reference already existed: - Issue: #{cross}##{ext_issue.iid} + Issue: #{ext_issue.to_reference(project)} These two references are introduced in an edit: - Issue: ##{new_issues[0].iid} - Cross: #{cross}##{new_issues[1].iid} + Issue: #{new_issues[0].to_reference} + Cross: #{new_issues[1].to_reference(project)} MSG # These three objects were already referenced, and should not receive new diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb index 06c39e1ada5..afbea55ab99 100644 --- a/spec/support/reference_filter_spec_helper.rb +++ b/spec/support/reference_filter_spec_helper.rb @@ -10,6 +10,26 @@ module ReferenceFilterSpecHelper Rails.application.routes.url_helpers end + # Modify a String reference to make it invalid + # + # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get + # their word characters reversed. + # + # reference - String reference to modify + # + # Returns a String + def invalidate_reference(reference) + if reference =~ /\A(.+)?.\d+\z/ + # Integer-based reference with optional project prefix + reference.gsub(/\d+\z/) { |i| i.to_i + 1 } + elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + # SHA-based reference with optional prefix + reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + else + reference.gsub(/\w+\z/) { |v| v.reverse } + end + end + # Perform `call` on the described class # # Automatically passes the current `project` value to the context if none is |