diff --git a/CHANGELOG b/CHANGELOG index ad15ed43b74416a8739e39f3322826e992f78f0d..7b2f152865645728e0f8defe49a789bbc02286d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ v 8.3.0 (unreleased) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - Fix 500 error when update group member permission - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera) + - Recognize issue/MR/snippet/commit links as references - Add ignore whitespace change option to commit view - Fire update hook from GitLab - Don't show project fork event as "imported" diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 931d52055f8314c63cbd5e3b2976f71e862c030f..e66b9c628c79d4053d927e553acedab6a6f95976 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -87,7 +87,11 @@ module IssuesHelper end def merge_requests_sentence(merge_requests) - merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') + # Sorting based on the `!123` or `group/project!123` reference will sort + # local merge requests first. + merge_requests.map do |merge_request| + merge_request.to_reference(@project) + end.sort.to_sentence(last_word_connector: ', or ') end def url_to_emoji(name) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 7f3a61a5e386bc7bad385872c3a33dfc82674e28..6c32647594d5a0642d70f52ed57cb93b5989badb 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -39,7 +39,11 @@ module MergeRequestsHelper end def issues_sentence(issues) - issues.map(&:to_reference).to_sentence + # Sorting based on the `#123` or `group/project#123` reference will sort + # local issues first. + issues.map do |issue| + issue.to_reference(@project) + end.sort.to_sentence end def mr_change_branches_path(merge_request) diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce39be2995c71c57ec2dfa0f2ce44c98..c0998a45709191d5c9b34086dca2699f9bc011c8 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -78,11 +78,23 @@ class Commit }x end + def self.link_reference_pattern + super("commit", /(?<commit>\h{6,40})/) + end + def to_reference(from_project = nil) if cross_project_reference?(from_project) - "#{project.to_reference}@#{id}" + project.to_reference + self.class.reference_prefix + self.id + else + self.id + end + end + + def reference_link_text(from_project = nil) + if cross_project_reference?(from_project) + project.to_reference + self.class.reference_prefix + self.short_id else - id + self.short_id end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 86fc9eb01a3e0077ce1a20a289c1a37474c109db..14e7971fa066175d79e5cdff056158ce5687698e 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -2,36 +2,38 @@ # # Examples: # -# range = CommitRange.new('f3f85602...e86e1013') +# range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false # range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # -# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true # range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # -# # Assuming `project` is a Project with a repository containing both commits: -# range.project = project +# # Assuming the specified project has a repository containing both commits: # range.valid_commits? # => true # class CommitRange include ActiveModel::Conversion include Referable - attr_reader :sha_from, :notation, :sha_to + attr_reader :commit_from, :notation, :commit_to + attr_reader :ref_from, :ref_to # Optional Project model attr_accessor :project - # See `exclude_start?` - attr_reader :exclude_start - - # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # The beginning and ending refs can be named or SHAs, and # the range notation can be double- or triple-dot. - PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/ + PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ + + # In text references, the beginning and ending refs can only be SHAs + # between 6 and 40 hex characters. + STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ def self.reference_prefix '@' @@ -43,27 +45,40 @@ class CommitRange def self.reference_pattern %r{ (?:#{Project.reference_pattern}#{reference_prefix})? - (?<commit_range>#{PATTERN}) + (?<commit_range>#{STRICT_PATTERN}) }x end + def self.link_reference_pattern + super("compare", /(?<commit_range>#{PATTERN})/) + end + # Initialize a CommitRange # # range_string - The String commit range. # project - An optional Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. - def initialize(range_string, project = nil) + def initialize(range_string, project) + @project = project + range_string.strip! - unless range_string.match(/\A#{PATTERN}\z/) + unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" end - @exclude_start = !range_string.include?('...') - @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2) - @project = project + if project.valid_repo? + @commit_from = project.commit(@ref_from) + @commit_to = project.commit(@ref_to) + end + + if valid_commits? + @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from) + @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to) + end end def inspect @@ -71,15 +86,24 @@ class CommitRange end def to_s - "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + sha_from + notation + sha_to end + alias_method :id, :to_s + 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) + project.to_reference + self.class.reference_prefix + self.id + else + self.id + end + end + + def reference_link_text(from_project = nil) + reference = ref_from + notation + ref_to if cross_project_reference?(from_project) - reference = project.to_reference + '@' + reference + reference = project.to_reference + self.class.reference_prefix + reference end reference @@ -87,46 +111,58 @@ class CommitRange # Returns a String for use in a link's title attribute def reference_title - "Commits #{suffixed_sha_from} through #{sha_to}" + "Commits #{sha_start} through #{sha_to}" end # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` def to_param - { from: suffixed_sha_from, to: sha_to } + { from: sha_start, to: sha_to } end def exclude_start? - exclude_start + @notation == '..' end # Check if both the starting and ending commit IDs exist in a project's # repository - # - # project - An optional Project to check (default: `project`) - def valid_commits?(project = project) - return nil unless project.present? - return false unless project.valid_repo? - - commit_from.present? && commit_to.present? + def valid_commits? + commit_start.present? && commit_end.present? end def persisted? true end - def commit_from - @commit_from ||= project.repository.commit(suffixed_sha_from) + def sha_from + return nil unless @commit_from + + @commit_from.id + end + + def sha_to + return nil unless @commit_to + + @commit_to.id end - def commit_to - @commit_to ||= project.repository.commit(sha_to) + def sha_start + return nil unless sha_from + + exclude_start? ? sha_from + '^' : sha_from end - private + def commit_start + return nil unless sha_start - def suffixed_sha_from - sha_from + (exclude_start? ? '^' : '') + if exclude_start? + @commit_start ||= project.commit(sha_start) + else + commit_from + end end + + alias_method :sha_end, :sha_to + alias_method :commit_end, :commit_to end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 193c91f1742940f4c87c1149ca2dec0d325a1c86..634a8d0f2747d3f327aed4eca3e167fbe8040480 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -62,13 +62,18 @@ module Mentionable return [] if text.blank? refs = all_references(current_user, text, load_lazy_references: load_lazy_references) - (refs.issues + refs.merge_requests + refs.commits) - [local_reference] + refs = (refs.issues + refs.merge_requests + refs.commits) + + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject { |ref| ref == local_reference } end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. def create_cross_references!(author = self.author, without = [], text = self.mentionable_text) refs = referenced_mentionables(author, text) - + # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. @@ -111,7 +116,7 @@ module Mentionable # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } end - + # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def cross_reference_exists?(target) diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index cced66cc1e4916ef84ea41395ab12aad7a4bacfb..ce064f675ae0387afbbf4a978d8e9baa13e18eb0 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -21,6 +21,10 @@ module Referable '' end + def reference_link_text(from_project = nil) + to_reference(from_project) + end + module ClassMethods # The character that prefixes the actual reference identifier # @@ -44,6 +48,25 @@ module Referable def reference_pattern raise NotImplementedError, "#{self} does not implement #{__method__}" end + + def link_reference_pattern(route, pattern) + %r{ + (?<url> + #{Regexp.escape(Gitlab.config.gitlab.url)} + \/#{Project.reference_pattern} + \/#{Regexp.escape(route)} + \/#{pattern} + (?<path> + (\/[a-z0-9_=-]+)* + )? + (?<query> + \?[a-z0-9_=-]+ + (&[a-z0-9_=-]+)* + )? + (?<anchor>\#[a-z0-9_-]+)? + ) + }x + end end private diff --git a/app/models/issue.rb b/app/models/issue.rb index 721831080334cefe53755d5df5d130af734a8931..187b6482b6cdc31b2658761699f9568314ecf8b0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -69,6 +69,10 @@ class Issue < ActiveRecord::Base }x end + def self.link_reference_pattern + super("issues", /(?<issue>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1b3d6079d2cdeeef0e9d9926a446cfb8276cbb32..2a4aee7e5d95e4501cf7ccee7a13fc4518f9c08b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -151,6 +151,10 @@ class MergeRequest < ActiveRecord::Base }x end + def self.link_reference_pattern + super("merge_requests", /(?<merge_request>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -316,7 +320,7 @@ class MergeRequest < ActiveRecord::Base issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) - issues.uniq.sort_by(&:id) + issues.uniq else [] end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b0831982aa799511194ae567a68a54e47e9470ce..f876be7a4c8540ad104e1c086282def4183dd6dc 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -65,6 +65,10 @@ class Snippet < ActiveRecord::Base }x end + def self.link_reference_pattern + super("snippets", /(?<snippet>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7e2bc834176a7b832ccf183a4f03cfa09253f074..09c159510cd376651fd741947a06742774873677 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -125,7 +125,7 @@ class SystemNoteService # Returns the created Note object def self.change_status(noteable, project, author, status, source) body = "Status changed to #{status}" - body += " by #{source.gfm_reference}" if source + body += " by #{source.gfm_reference(project)}" if source create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index aef352029d06d5425d2bee39b108af321c49dfb5..917d5181689d99a75627e1cc79b264f533e9e97a 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,3 @@ .issue-closed-by-widget = icon('check') - This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted. + This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests))} is accepted. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 62619241001731fc30d64de0224c83ede6342355..63d8ae174365bc7f6613a109a42671745b573191 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -164,7 +164,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled']. Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index aeec595782c53fb012dcfc3fa00e67d37db27861..9bef9037ad6dda28ed0480d3a75d7afde0840c9e 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -1,6 +1,12 @@ module Gitlab class ClosingIssueExtractor - ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern) + ISSUE_CLOSING_REGEX = begin + link_pattern = URI.regexp(%w(http https)) + + pattern = Gitlab.config.gitlab.issue_closing_pattern + pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))") + Regexp.new(pattern).freeze + end def initialize(project, current_user = nil) @extractor = Gitlab::ReferenceExtractor.new(project, current_user) @@ -9,10 +15,12 @@ module Gitlab def closed_by_message(message) return [] if message.nil? - closing_statements = message.scan(ISSUE_CLOSING_REGEX). - map { |ref| ref[0] }.join(" ") + closing_statements = [] + message.scan(ISSUE_CLOSING_REGEX) do + closing_statements << Regexp.last_match[0] + end - @extractor.analyze(closing_statements) + @extractor.analyze(closing_statements.join(" ")) @extractor.issues end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index b082bfc434bca43f89185a53e5e72db89015f1a6..886a09f52af7d0b1fb36751a0377f044bc3a02af 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -178,7 +178,6 @@ module Gitlab Gitlab::Markdown::SanitizationFilter, Gitlab::Markdown::UploadLinkFilter, - Gitlab::Markdown::RelativeLinkFilter, Gitlab::Markdown::EmojiFilter, Gitlab::Markdown::TableOfContentsFilter, Gitlab::Markdown::AutolinkFilter, @@ -193,6 +192,8 @@ module Gitlab Gitlab::Markdown::CommitReferenceFilter, Gitlab::Markdown::LabelReferenceFilter, + Gitlab::Markdown::RelativeLinkFilter, + Gitlab::Markdown::TaskListFilter ] end diff --git a/lib/gitlab/markdown/abstract_reference_filter.rb b/lib/gitlab/markdown/abstract_reference_filter.rb index fd5b7eb9332e72715286415138f48740b727dbe3..9488e980c086f22756e2f59eb6ec2fee3d9ff501 100644 --- a/lib/gitlab/markdown/abstract_reference_filter.rb +++ b/lib/gitlab/markdown/abstract_reference_filter.rb @@ -2,8 +2,8 @@ require 'gitlab/markdown' module Gitlab module Markdown - # Issues, Snippets and Merge Requests shares similar functionality in refernce filtering. - # All this functionality moved to this class + # Issues, Merge Requests, Snippets, Commits and Commit Ranges share + # similar functionality in reference filtering. class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference @@ -26,21 +26,20 @@ module Gitlab # Public: Find references in text (like `!123` for merge requests) # - # AnyReferenceFilter.references_in(text) do |match, object| - # "<a href=...>PREFIX#{object}</a>" + # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| + # object = find_object(project_ref, id) + # "<a href=...>#{object.to_reference}</a>" # end # - # PREFIX - symbol that detects reference (like ! for merge requests) - # object - reference object (snippet, merget request etc) # text - String text to search. # - # Yields the String match, the Integer referenced object ID, and an optional String - # of the external project reference. + # Yields the String match, the Integer referenced object ID, an optional String + # of the external project reference, and all of the matchdata. # # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(object_class.reference_pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project] + def self.references_in(text, pattern = object_class.reference_pattern) + text.gsub(pattern) do |match| + yield match, $~[object_sym].to_i, $~[:project], $~ end end @@ -61,8 +60,27 @@ module Gitlab end def call + # `#123` replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content) + object_link_filter(content, object_class.reference_pattern) + end + + # `[Issue](#123)`, which is turned into + # `<a href="#123">Issue</a>` + replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| + object_link_filter(link, object_class.reference_pattern, link_text: text) + end + + # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into + # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>` + replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| + object_link_filter(text, object_class.link_reference_pattern) + end + + # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into + # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>` + replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| + object_link_filter(link, object_class.link_reference_pattern, link_text: text) end end @@ -70,30 +88,57 @@ module Gitlab # to the referenced object's details page. # # text - String text to replace references in. + # pattern - Reference pattern to match against. + # link_text - Original content of the link being replaced. # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text) - references_in(text) do |match, id, project_ref| + def object_link_filter(text, pattern, link_text: nil) + references_in(text, pattern) do |match, id, project_ref, matches| project = project_from_ref(project_ref) if project && object = find_object(project, id) - title = escape_once("#{object_title}: #{object.title}") + title = escape_once(object_link_title(object)) klass = reference_class(object_sym) - data = data_attribute(project: project.id, object_sym => object.id) - url = url_for_object(object, project) + + data = data_attribute( + original: link_text || match, + project: project.id, + object_sym => object.id + ) + + url = matches[:url] if matches.names.include?("url") + url ||= url_for_object(object, project) + + text = link_text + unless text + text = object.reference_link_text(context[:project]) + + extras = object_link_text_extras(object, matches) + text += " (#{extras.join(", ")})" if extras.any? + end %(<a href="#{url}" #{data} title="#{title}" - class="#{klass}">#{match}</a>) + class="#{klass}">#{text}</a>) else match end end end - def object_title - object_class.name.titleize + def object_link_text_extras(object, matches) + extras = [] + + if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ + extras << "comment #{$1}" + end + + extras + end + + def object_link_title(object) + "#{object_class.name.titleize}: #{object.title}" end end end diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index e070edae0a4c43f7747c4a207955f051ebbe265f..36b3258ef761077974e1d070684bae07d5bf6dd6 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit range references with links. # # This filter supports cross-project references. - class CommitRangeReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitRangeReferenceFilter < AbstractReferenceFilter + def self.object_class + CommitRange + end - # Public: Find commit range references in text - # - # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref| - # "<a href=...>#{commit_range}</a>" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit range, and an optional String - # of the external project reference. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(CommitRange.reference_pattern) do |match| - yield match, $~[:commit_range], $~[:project] + def self.references_in(text, pattern = CommitRange.reference_pattern) + text.gsub(pattern) do |match| + yield match, $~[:commit_range], $~[:project], $~ end end @@ -31,9 +21,9 @@ module Gitlab return unless project id = node.attr("data-commit-range") - range = CommitRange.new(id, project) + range = find_object(project, id) - return unless range.valid_commits? + return unless range { commit_range: range } end @@ -44,49 +34,25 @@ module Gitlab @commit_map = {} end - def call - replace_text_nodes_matching(CommitRange.reference_pattern) do |content| - commit_range_link_filter(content) - end - end - - # Replace commit range references in text with links to compare the commit - # ranges. - # - # text - String text to replace references in. - # - # Returns a String with commit range references replaced with links. All - # links have `gfm` and `gfm-commit_range` class names attached for - # styling. - def commit_range_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - range = CommitRange.new(id, project) - - if range.valid_commits? - url = url_for_commit_range(project, range) - - title = range.reference_title - klass = reference_class(:commit_range) - data = data_attribute(project: project.id, commit_range: id) + def self.find_object(project, id) + range = CommitRange.new(id, project) - project_ref += '@' if project_ref + range.valid_commits? ? range : nil + end - %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{project_ref}#{range}</a>) - else - match - end - end + def find_object(*args) + self.class.find_object(*args) end - def url_for_commit_range(project, range) + def url_for_object(range, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end + + def object_link_title(range) + range.reference_title + end end end end diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index 8cdbeb1f9cf7101b0201a65003c125e9f8e5ffcf..b4036578e60ed753f5baa6c16e16ee98a0bab6cd 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -5,24 +5,14 @@ module Gitlab # HTML filter that replaces commit references with links. # # This filter supports cross-project references. - class CommitReferenceFilter < ReferenceFilter - include CrossProjectReference + class CommitReferenceFilter < AbstractReferenceFilter + def self.object_class + Commit + end - # Public: Find commit references in text - # - # CommitReferenceFilter.references_in(text) do |match, commit, project_ref| - # "<a href=...>#{commit}</a>" - # end - # - # text - String text to search. - # - # Yields the String match, the String commit identifier, and an optional - # String of the external project reference. - # - # Returns a String replaced with the return of the block. - def self.references_in(text) - text.gsub(Commit.reference_pattern) do |match| - yield match, $~[:commit], $~[:project] + def self.references_in(text, pattern = Commit.reference_pattern) + text.gsub(pattern) do |match| + yield match, $~[:commit], $~[:project], $~ end end @@ -31,58 +21,32 @@ module Gitlab return unless project id = node.attr("data-commit") - commit = commit_from_ref(project, id) + commit = find_object(project, id) return unless commit { commit: commit } end - def call - replace_text_nodes_matching(Commit.reference_pattern) do |content| - commit_link_filter(content) - end - end - - # Replace commit references in text with links to the commit specified. - # - # text - String text to replace references in. - # - # Returns a String with commit references replaced with links. All links - # have `gfm` and `gfm-commit` class names attached for styling. - def commit_link_filter(text) - self.class.references_in(text) do |match, id, project_ref| - project = self.project_from_ref(project_ref) - - if commit = self.class.commit_from_ref(project, id) - url = url_for_commit(project, commit) - - title = escape_once(commit.link_title) - klass = reference_class(:commit) - data = data_attribute(project: project.id, commit: id) - - project_ref += '@' if project_ref - - %(<a href="#{url}" #{data} - title="#{title}" - class="#{klass}">#{project_ref}#{commit.short_id}</a>) - else - match - end - end - end - - def self.commit_from_ref(project, id) + def self.find_object(project, id) if project && project.valid_repo? project.commit(id) end end - def url_for_commit(project, commit) + def find_object(*args) + self.class.find_object(*args) + end + + def url_for_object(commit, project) h = Gitlab::Application.routes.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end + + def object_link_title(commit) + commit.link_title + end end end end diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb index 8f86f13976abc847adb81542675fb4177a970c1d..14bdf5521fc5fd1ddae486ba496d323cb75a1c18 100644 --- a/lib/gitlab/markdown/external_issue_reference_filter.rb +++ b/lib/gitlab/markdown/external_issue_reference_filter.rb @@ -30,6 +30,10 @@ module Gitlab replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| issue_link_filter(content) end + + replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text| + issue_link_filter(link, link_text: text) + end end # Replace `JIRA-123` issue references in text with links to the referenced @@ -39,7 +43,7 @@ module Gitlab # # Returns a String with `JIRA-123` references replaced with links. All # links have `gfm` and `gfm-issue` class names attached for styling. - def issue_link_filter(text) + def issue_link_filter(text, link_text: nil) project = context[:project] self.class.references_in(text) do |match, issue| @@ -49,9 +53,11 @@ module Gitlab klass = reference_class(:issue) data = data_attribute(project: project.id) + text = link_text || match + %(<a href="#{url}" #{data} title="#{title}" - class="#{klass}">#{match}</a>) + class="#{klass}">#{text}</a>) end end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb index 29e51b6ade672d056230e683490d3d4ea95d6dec..e09dfcb83c87ac8d5cc295ca2e4b81d3b3ff196c 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -8,9 +8,9 @@ module Gitlab class ExternalLinkFilter < HTML::Pipeline::Filter def call doc.search('a').each do |node| - next unless node.has_attribute?('href') + link = node.attr('href') - link = node.attribute('href').value + next unless link # Skip non-HTTP(S) links next unless link.start_with?('http') diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 13581b8fb136671a0c51b8dd74e8e1e2a0b22f65..a2026eecaeb9018488378dc619f99d71dad4037d 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -30,6 +30,10 @@ module Gitlab replace_text_nodes_matching(Label.reference_pattern) do |content| label_link_filter(content) end + + replace_link_nodes_with_href(Label.reference_pattern) do |link, text| + label_link_filter(link, link_text: text) + end end # Replace label references in text with links to the label specified. @@ -38,7 +42,7 @@ module Gitlab # # Returns a String with label references replaced with links. All links # have `gfm` and `gfm-label` class names attached for styling. - def label_link_filter(text) + def label_link_filter(text, link_text: nil) project = context[:project] self.class.references_in(text) do |match, id, name| @@ -47,10 +51,16 @@ module Gitlab if label = project.labels.find_by(params) url = url_for_label(project, label) klass = reference_class(:label) - data = data_attribute(project: project.id, label: label.id) + data = data_attribute( + original: link_text || match, + project: project.id, + label: label.id + ) + + text = link_text || render_colored_label(label) %(<a href="#{url}" #{data} - class="#{klass}">#{render_colored_label(label)}</a>) + class="#{klass}">#{text}</a>) else match end @@ -59,8 +69,8 @@ module Gitlab def url_for_label(project, label) h = Gitlab::Application.routes.url_helpers - h.namespace_project_issues_path(project.namespace, project, - label_name: label.name) + h.namespace_project_issues_url( project.namespace, project, label_name: label.name, + only_path: context[:only_path]) end def render_colored_label(label) diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index 1f47f03c94ebf5f75041c23b2b80cee7fcc0cb4d..de71fc76a9b3e8f07e108242d6b4b5464ef7a39d 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -20,6 +20,16 @@ module Gitlab h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end + + def object_link_text_extras(object, matches) + extras = super + + if matches.names.include?("path") && matches[:path] && matches[:path] == '/diffs' + extras.unshift "diffs" + end + + extras + end end end end diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb index a1f3a8a8ebfc180a02fcf64b437947f9f6426e07..bea714a01e75d4d18140ef1db16fd86de3cb5646 100644 --- a/lib/gitlab/markdown/redactor_filter.rb +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -12,7 +12,10 @@ module Gitlab def call doc.css('a.gfm').each do |node| unless user_can_reference?(node) - node.replace(node.text) + # The reference should be replaced by the original text, + # which is not always the same as the rendered text. + text = node.attr('data-original') || node.text + node.replace(text) end end diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index a4c560f578cae024b180fb3d05c0f0dd6e85f215..b6d93e05ec7987ac198679902451b394c319ff86 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -122,6 +122,80 @@ module Gitlab doc end + # Iterate through the document's link nodes, yielding the current node's + # content if: + # + # * The `project` context value is present AND + # * The node's content matches `pattern` + # + # pattern - Regex pattern against which to match the node's content + # + # Yields the current node's String contents. The result of the block will + # replace the node and update the current document. + # + # Returns the updated Nokogiri::HTML::DocumentFragment object. + def replace_link_nodes_with_text(pattern) + return doc if project.nil? + + doc.search('a').each do |node| + klass = node.attr('class') + next if klass && klass.include?('gfm') + + link = node.attr('href') + text = node.text + + next unless link && text + + link = URI.decode(link) + # Ignore ending punctionation like periods or commas + next unless link == text && text =~ /\A#{pattern}/ + + html = yield text + + next if html == text + + node.replace(html) + end + + doc + end + + # Iterate through the document's link nodes, yielding the current node's + # content if: + # + # * The `project` context value is present AND + # * The node's HREF matches `pattern` + # + # pattern - Regex pattern against which to match the node's HREF + # + # Yields the current node's String HREF and String content. + # The result of the block will replace the node and update the current document. + # + # Returns the updated Nokogiri::HTML::DocumentFragment object. + def replace_link_nodes_with_href(pattern) + return doc if project.nil? + + doc.search('a').each do |node| + klass = node.attr('class') + next if klass && klass.include?('gfm') + + link = node.attr('href') + text = node.text + + next unless link && text + link = URI.decode(link) + next unless link && link =~ /\A#{pattern}\z/ + + html = yield link, text + + next if html == link + + node.replace(html) + end + + doc + end + # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 632be4d754255bbf4803dbe3c6f2e12ace6a4b9a..692c51fd324ad8783ed9204159c3e259aa278023 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -17,6 +17,9 @@ module Gitlab return doc unless linkable_files? doc.search('a').each do |el| + klass = el.attr('class') + next if klass && klass.include?('gfm') + process_link_attr el.attribute('href') end diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index ab5e1f6fe9eda11e4794b8fb6db570546b1eca26..0a20d9c034729e660e51f1f211307f1b98bd2f76 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -52,6 +52,10 @@ module Gitlab replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) end + + replace_link_nodes_with_href(User.reference_pattern) do |link, text| + user_link_filter(link, link_text: text) + end end # Replace `@user` user references in text with links to the referenced @@ -61,12 +65,12 @@ module Gitlab # # Returns a String with `@user` references replaced with links. All links # have `gfm` and `gfm-project_member` class names attached for styling. - def user_link_filter(text) + def user_link_filter(text, link_text: nil) self.class.references_in(text) do |match, username| if username == 'all' - link_to_all + link_to_all(link_text: link_text) elsif namespace = Namespace.find_by(path: username) - link_to_namespace(namespace) || match + link_to_namespace(namespace, link_text: link_text) || match else match end @@ -83,36 +87,36 @@ module Gitlab reference_class(:project_member) end - def link_to_all + def link_to_all(link_text: nil) project = context[:project] url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) data = data_attribute(project: project.id) - text = User.reference_prefix + 'all' + text = link_text || User.reference_prefix + 'all' link_tag(url, data, text) end - def link_to_namespace(namespace) + def link_to_namespace(namespace, link_text: nil) if namespace.is_a?(Group) - link_to_group(namespace.path, namespace) + link_to_group(namespace.path, namespace, link_text: link_text) else - link_to_user(namespace.path, namespace) + link_to_user(namespace.path, namespace, link_text: link_text) end end - def link_to_group(group, namespace) + def link_to_group(group, namespace, link_text: nil) url = urls.group_url(group, only_path: context[:only_path]) data = data_attribute(group: namespace.id) - text = Group.reference_prefix + group + text = link_text || Group.reference_prefix + group link_tag(url, data, text) end - def link_to_user(user, namespace) + def link_to_user(user, namespace, link_text: nil) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) - text = User.reference_prefix + user + text = link_text || User.reference_prefix + user link_tag(url, data, text) end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index da8df8a3025550e5a23d73c19c303fd0894464ef..3c3478a12719e9a66c639918a41fe2b2740a6223 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -41,14 +41,14 @@ module Gitlab # Returns the results Array for the requested filter type def pipeline_result(filter_type) return [] if @text.blank? - + klass = "#{filter_type.to_s.camelize}ReferenceFilter" filter = Gitlab::Markdown.const_get(klass) context = { project: project, current_user: current_user, - + # We don't actually care about the links generated only_path: true, ignore_blockquotes: true, @@ -58,7 +58,15 @@ module Gitlab reference_filter: filter } - pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context) + # We need to autolink first to finds links to referables, and to prevent + # numeric anchors to be parsed as issue references. + filters = [ + Gitlab::Markdown::AutolinkFilter, + filter, + Gitlab::Markdown::ReferenceGathererFilter + ] + + pipeline = HTML::Pipeline.new(filters, context) result = pipeline.call(@text) values = result[:references][filter_type].uniq diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 41d12afa9ce8f41561c7f960f9e87f567bec0a06..e8dfc5c0eb173c5a8677ba7def2e8c09612338c0 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -153,6 +153,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Ignores invalid: <%= User.reference_prefix %>fake_user - Ignored in code: `<%= user.to_reference %>` - Ignored in links: [Link to <%= user.to_reference %>](#user-link) +- Link to user by reference: [User](<%= user.to_reference %>) #### IssueReferenceFilter @@ -160,6 +161,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Issue in another project: <%= xissue.to_reference(project) %> - Ignored in code: `<%= issue.to_reference %>` - Ignored in links: [Link to <%= issue.to_reference %>](#issue-link) +- Issue by URL: <%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %> +- Link to issue by reference: [Issue](<%= issue.to_reference %>) +- Link to issue by URL: [Issue](<%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>) #### MergeRequestReferenceFilter @@ -167,6 +171,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - 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) +- Merge request by URL: <%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %> +- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>) +- Link to merge request by URL: [Merge request](<%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>) #### SnippetReferenceFilter @@ -174,6 +181,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Snippet in another project: <%= xsnippet.to_reference(project) %> - Ignored in code: `<%= snippet.to_reference %>` - Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link) +- Snippet by URL: <%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %> +- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>) +- Link to snippet by URL: [Snippet](<%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>) #### CommitRangeReferenceFilter @@ -181,6 +191,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - 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) +- Range by URL: <%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %> +- Link to range by reference: [Range](<%= commit_range.to_reference %>) +- Link to range by URL: [Range](<%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>) #### CommitReferenceFilter @@ -188,6 +201,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Commit in another project: <%= xcommit.to_reference(project) %> - Ignored in code: `<%= commit.to_reference %>` - Ignored in links: [Link to <%= commit.to_reference %>](#commit-link) +- Commit by URL: <%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %> +- Link to commit by reference: [Commit](<%= commit.to_reference %>) +- Link to commit by URL: [Commit](<%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>) #### LabelReferenceFilter @@ -196,6 +212,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - 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) +- Link to label by reference: [Label](<%= label.to_reference %>) ### Task Lists diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 21254f778d3f39798b1a88f276aea8f35e718926..fe1b94a484e98ec6ca3bfbea73912e45915ed92f 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -2,11 +2,18 @@ require 'spec_helper' describe Gitlab::ClosingIssueExtractor do let(:project) { create(:project) } + let(:project2) { create(:project) } let(:issue) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project2) } let(:reference) { issue.to_reference } + let(:cross_reference) { issue2.to_reference(project) } subject { described_class.new(project, project.creator) } + before do + project2.team << [project.creator, :master] + end + describe "#closed_by_message" do context 'with a single reference' do it do @@ -130,6 +137,27 @@ describe Gitlab::ClosingIssueExtractor do end end + context "with a cross-project reference" do + it do + message = "Closes #{cross_reference}" + expect(subject.closed_by_message(message)).to eq([issue2]) + end + end + + context "with a cross-project URL" do + it do + message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)}" + expect(subject.closed_by_message(message)).to eq([issue2]) + end + end + + context "with an invalid URL" do + it do + message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}" + expect(subject.closed_by_message(message)).to eq([]) + end + end + context 'with multiple references' do let(:other_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) } @@ -171,6 +199,31 @@ describe Gitlab::ClosingIssueExtractor do expect(subject.closed_by_message(message)). to match_array([issue, other_issue, third_issue]) end + + it "fetches cross-project references" do + message = "Closes #{reference} and #{cross_reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue, issue2]) + end + + it "fetches cross-project URL references" do + message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)} and #{reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue, issue2]) + end + + it "ignores invalid cross-project URL references" do + message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)} and #{reference}" + + expect(subject.closed_by_message(message)). + to match_array([issue]) + end end end + + def urls + Gitlab::Application.routes.url_helpers + end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index e5b8d723fe5614652cc98000511f8250d554d1d7..9ce63f9af464e18f10e155e5903ada0c409397cc 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -5,11 +5,11 @@ module Gitlab::Markdown include FilterSpecHelper let(:project) { create(:project, :public) } - let(:commit1) { project.commit } - let(:commit2) { project.commit("HEAD~2") } + let(:commit1) { project.commit("HEAD~2") } + let(:commit2) { project.commit } - let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") } - let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") } + let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) } + let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -18,7 +18,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -27,14 +27,14 @@ module Gitlab::Markdown let(:reference2) { range2.to_reference } it 'links to a valid two-dot reference' do - doc = filter("See #{reference2}") + doc = reference_filter("See #{reference2}") expect(doc.css('a').first.attr('href')). 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}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param) @@ -46,14 +46,14 @@ module Gitlab::Markdown exp = commit1.short_id + '...' + commit2.short_id - expect(filter("See #{reference}").css('a').first.text).to eq exp - expect(filter("See #{reference2}").css('a').first.text).to eq exp + expect(reference_filter("See #{reference}").css('a').first.text).to eq exp + expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") - exp = Regexp.escape(range.to_s) + exp = Regexp.escape(range.reference_link_text) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end @@ -62,21 +62,22 @@ module Gitlab::Markdown expect(project).to receive(:valid_repo?).and_return(true) expect(project.repository).to receive(:commit).with(commit1.id.reverse) - expect(filter(act).to_html).to eq exp + expect(project.repository).to receive(:commit).with(commit2.id) + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('title')).to eq range.reference_title end it 'includes default classes' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' end it 'includes a data-project attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -84,15 +85,15 @@ module Gitlab::Markdown end it 'includes a data-commit-range attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-commit-range') - expect(link.attr('data-commit-range')).to eq range.to_reference + expect(link.attr('data-commit-range')).to eq range.to_s end it 'supports an :only_path option' do - doc = filter("See #{reference}", only_path: true) + doc = reference_filter("See #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -115,25 +116,63 @@ module Gitlab::Markdown end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") - exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}") expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end it 'ignores invalid commit IDs on the referenced project' do exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:range) { CommitRange.new("#{commit1.id}...master", project) } + let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') } + + before do + range.project = project2 + end + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + exp = Regexp.escape(range.reference_link_text(project)) + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" + expect(reference_filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(reference_filter(act).to_html).to eq exp end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index d080efbf3d4fa011a7cea580601322c7120c2189..462a41b47562958a43497a0aa289da384dee2ec4 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -14,7 +14,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -24,7 +24,7 @@ module Gitlab::Markdown # Let's test a variety of commit SHA sizes just to be paranoid [6, 8, 12, 18, 20, 32, 40].each do |size| it "links to a valid reference of #{size} characters" do - doc = filter("See #{reference[0...size]}") + doc = reference_filter("See #{reference[0...size]}") expect(doc.css('a').first.text).to eq commit.short_id expect(doc.css('a').first.attr('href')). @@ -33,15 +33,15 @@ module Gitlab::Markdown end it 'always uses the short ID as the link text' do - doc = filter("See #{commit.id}") + doc = reference_filter("See #{commit.id}") expect(doc.text).to eq "See #{commit.short_id}" - doc = filter("See #{commit.id[0...6]}") + doc = reference_filter("See #{commit.id[0...6]}") expect(doc.text).to eq "See #{commit.short_id}" end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/) end @@ -51,28 +51,28 @@ module Gitlab::Markdown expect(project).to receive(:valid_repo?).and_return(true) expect(project.repository).to receive(:commit).with(invalid) - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('title')).to eq commit.link_title end it 'escapes the title attribute' do allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.text).to eq "See #{commit.short_id}" end it 'includes default classes' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' end it 'includes a data-project attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -80,7 +80,7 @@ module Gitlab::Markdown end it 'includes a data-commit attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-commit') @@ -88,7 +88,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("See #{reference}", only_path: true) + doc = reference_filter("See #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -108,14 +108,14 @@ module Gitlab::Markdown let(:reference) { commit.to_reference(project) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") exp = Regexp.escape(project2.to_reference) expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) @@ -123,7 +123,37 @@ module Gitlab::Markdown it 'ignores invalid commit IDs on the referenced project' do exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:commit) { project2.commit } + let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + act = "Committed #{invalidate_reference(reference)}" + expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 94c80ae6611aeb2221ecabc6b57d6971de02887c..078ff3ed4b2de9c43050d00c4b0b34ddded492aa 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -18,7 +18,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -29,18 +29,18 @@ module Gitlab::Markdown expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'links to a valid reference' do - doc = filter("Fixed #{reference}") + doc = reference_filter("Fixed #{reference}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end @@ -48,28 +48,28 @@ module Gitlab::Markdown invalid = invalidate_reference(reference) exp = act = "Fixed #{invalid}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Issue #{reference}") + doc = reference_filter("Issue #{reference}") expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" end it 'escapes the title attribute' do issue.update_attribute(:title, %{"></a>whatever<a title="}) - doc = filter("Issue #{reference}") + doc = reference_filter("Issue #{reference}") expect(doc.text).to eq "Issue #{reference}" end it 'includes default classes' do - doc = filter("Issue #{reference}") + doc = reference_filter("Issue #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' end it 'includes a data-project attribute' do - doc = filter("Issue #{reference}") + doc = reference_filter("Issue #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -77,7 +77,7 @@ module Gitlab::Markdown end it 'includes a data-issue attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-issue') @@ -85,7 +85,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("Issue #{reference}", only_path: true) + doc = reference_filter("Issue #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -109,25 +109,97 @@ module Gitlab::Markdown with(issue.iid).and_return(nil) exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project2) end it 'links with adjacent text' do - doc = filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid issue IDs on the referenced project' do exp = act = "Fixed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project reference in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] + end + end + + context 'cross-project URL in link href' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + "#note_123" + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index ae286c8be2b2123b9bae9c88f5e6e8f4b3b20e60..ef6dd524aba51efbb71c3243eed03ff527d1376a 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -16,17 +16,17 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Label #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end it 'includes default classes' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' end it 'includes a data-project attribute' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -34,7 +34,7 @@ module Gitlab::Markdown end it 'includes a data-label attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-label') @@ -42,7 +42,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("Label #{reference}", only_path: true) + doc = reference_filter("Label #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -56,33 +56,33 @@ module Gitlab::Markdown describe 'label span element' do it 'includes default classes' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a span').first.attr('class')).to eq 'label color-label' end it 'includes a style attribute' do - doc = filter("Label #{reference}") + doc = reference_filter("Label #{reference}") expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) end end context 'Integer-based references' do it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + namespace_project_issues_url(project.namespace, project, label_name: label.name) end it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") + doc = reference_filter("Label (#{reference}.)") expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) end it 'ignores invalid label IDs' do exp = act = "Label #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -91,22 +91,22 @@ module Gitlab::Markdown let(:reference) { "#{Label.reference_prefix}#{label.name}" } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + namespace_project_issues_url(project.namespace, project, label_name: label.name) expect(doc.text).to eq 'See gfm' end it 'links with adjacent text' do - doc = filter("Label (#{reference}.)") + doc = reference_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.reference_prefix}#{label.name.reverse}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -115,29 +115,66 @@ module Gitlab::Markdown let(:reference) { label.to_reference(:name) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. - namespace_project_issues_path(project.namespace, project, label_name: label.name) + 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}.)") + doc = reference_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.reference_prefix}"#{label.name.reverse}") - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end describe 'edge cases' do it 'gracefully handles non-references matching the pattern' do exp = act = '(format nil "~0f" 3.0) ; 3.0' - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a label in a link href' do + let(:reference) { %Q{<a href="#{label.to_reference}">Label</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = reference_filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\))) + end + + it 'includes a data-project attribute' do + doc = reference_filter("Label #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-label attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-label') + expect(link.attr('data-label')).to eq label.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Label #{reference}") + expect(result[:references][:label]).to eq [label] end end end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 3ef6cdfff33b11ca5f39a87c5f069608c497b904..4a23205112714e719f9806144dcf0e1500cbe7a9 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -14,7 +14,7 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -22,42 +22,42 @@ module Gitlab::Markdown let(:reference) { merge.to_reference } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_merge_request_url(project.namespace, project, merge) end it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") + doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid merge IDs' do exp = act = "Merge #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Merge #{reference}") + doc = reference_filter("Merge #{reference}") expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" end it 'escapes the title attribute' do merge.update_attribute(:title, %{"></a>whatever<a title="}) - doc = filter("Merge #{reference}") + doc = reference_filter("Merge #{reference}") expect(doc.text).to eq "Merge #{reference}" end it 'includes default classes' do - doc = filter("Merge #{reference}") + doc = reference_filter("Merge #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' end it 'includes a data-project attribute' do - doc = filter("Merge #{reference}") + doc = reference_filter("Merge #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -65,7 +65,7 @@ module Gitlab::Markdown end it 'includes a data-merge-request attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-merge-request') @@ -73,7 +73,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("Merge #{reference}", only_path: true) + doc = reference_filter("Merge #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -89,26 +89,50 @@ module Gitlab::Markdown context 'cross-project reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } - let(:merge) { create(:merge_request, source_project: project2) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } let(:reference) { merge.to_reference(project) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_merge_request_url(project2.namespace, - project, merge) + project2, merge) end it 'links with adjacent text' do - doc = filter("Merge (#{reference}.)") + doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid merge IDs on the referenced project' do exp = act = "Merge #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2, target_project: project2) } + let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq reference + end + + it 'links with adjacent text' do + doc = reference_filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index 9d9652dba46b1a3f0badd95de2cabdaa41cc6266..3a9acc9d6d4b6598b79910d9d72e415883232369 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -15,48 +15,48 @@ module Gitlab::Markdown %w(pre code a style).each do |elem| it "ignores valid references contained inside '#{elem}' element" do exp = act = "<#{elem}>Snippet #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end context 'internal reference' do it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')).to eq urls. namespace_project_snippet_url(project.namespace, project, snippet) end it 'links with adjacent text' do - doc = filter("Snippet (#{reference}.)") + doc = reference_filter("Snippet (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid snippet IDs' do exp = act = "Snippet #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end it 'includes a title attribute' do - doc = filter("Snippet #{reference}") + doc = reference_filter("Snippet #{reference}") expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" end it 'escapes the title attribute' do snippet.update_attribute(:title, %{"></a>whatever<a title="}) - doc = filter("Snippet #{reference}") + doc = reference_filter("Snippet #{reference}") expect(doc.text).to eq "Snippet #{reference}" end it 'includes default classes' do - doc = filter("Snippet #{reference}") + doc = reference_filter("Snippet #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' end it 'includes a data-project attribute' do - doc = filter("Snippet #{reference}") + doc = reference_filter("Snippet #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -64,7 +64,7 @@ module Gitlab::Markdown end it 'includes a data-snippet attribute' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-snippet') @@ -72,7 +72,7 @@ module Gitlab::Markdown end it 'supports an :only_path context' do - doc = filter("Snippet #{reference}", only_path: true) + doc = reference_filter("Snippet #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) @@ -92,21 +92,51 @@ module Gitlab::Markdown let(:reference) { snippet.to_reference(project) } it 'links to a valid reference' do - doc = filter("See #{reference}") + doc = reference_filter("See #{reference}") expect(doc.css('a').first.attr('href')). to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) end it 'links with adjacent text' do - doc = filter("See (#{reference}.)") + doc = reference_filter("See (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end it 'ignores invalid snippet IDs on the referenced project' do exp = act = "See #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] + end + end + + context 'cross-project URL reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, :public, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end it 'adds to the results hash' do diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index d9e0d7c42db7fd1bc83981d4b5bdeb0600596fbd..25379f0670ef198bde870d35d9d8aacc3c307bca 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -14,13 +14,13 @@ module Gitlab::Markdown it 'ignores invalid users' do exp = act = "Hey #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq(exp) + expect(reference_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 #{reference}</#{elem}>" - expect(filter(act).to_html).to eq exp + expect(reference_filter(act).to_html).to eq exp end end @@ -32,7 +32,7 @@ module Gitlab::Markdown end it 'supports a special @all mention' do - doc = filter("Hey #{reference}") + doc = reference_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) @@ -46,26 +46,26 @@ module Gitlab::Markdown context 'mentioning a user' do it 'links to a User' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) end it 'links to a User with a period' do user = create(:user, name: 'alphA.Beta') - doc = filter("Hey #{user.to_reference}") + doc = reference_filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end it 'links to a User with an underscore' do user = create(:user, name: 'ping_pong_king') - doc = filter("Hey #{user.to_reference}") + doc = reference_filter("Hey #{user.to_reference}") expect(doc.css('a').length).to eq 1 end it 'includes a data-user attribute' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-user') @@ -83,12 +83,12 @@ module Gitlab::Markdown let(:reference) { group.to_reference } it 'links to the Group' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) end it 'includes a data-group attribute' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-group') @@ -102,21 +102,48 @@ module Gitlab::Markdown end it 'links with adjacent text' do - doc = filter("Mention me (#{reference}.)") + doc = reference_filter("Mention me (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) end it 'includes default classes' do - doc = filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' end it 'supports an :only_path context' do - doc = filter("Hey #{reference}", only_path: true) + doc = reference_filter("Hey #{reference}", only_path: true) link = doc.css('a').first.attr('href') expect(link).not_to match %r(https?://) expect(link).to eq urls.user_path(user) end + + context 'referencing a user in a link href' do + let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} } + + it 'links to a User' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/) + end + + it 'includes a data-user attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq [user] + end + end end end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 1031af097bd0c396d043c3186f459b3f82dc16ac..3c1009a2eb087267cbd3d674a42077ca9163556c 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -7,50 +7,72 @@ describe CommitRange do it { is_expected.to include_module(Referable) } end - let(:sha_from) { 'f3f85602' } - let(:sha_to) { 'e86e1013' } + let!(:project) { create(:project, :public) } + let!(:commit1) { project.commit("HEAD~2") } + let!(:commit2) { project.commit } - let(:range) { described_class.new("#{sha_from}...#{sha_to}") } - let(:range2) { described_class.new("#{sha_from}..#{sha_to}") } + let(:sha_from) { commit1.short_id } + let(:sha_to) { commit2.short_id } + + let(:full_sha_from) { commit1.id } + let(:full_sha_to) { commit2.id } + + let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } + let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } it 'raises ArgumentError when given an invalid range string' do - expect { described_class.new("Foo") }.to raise_error(ArgumentError) + expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) end describe '#to_s' do it 'is correct for three-dot syntax' do - expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}" + expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}" end it 'is correct for two-dot syntax' do - expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}" + expect(range2.to_s).to eq "#{full_sha_from}..#{full_sha_to}" end end describe '#to_reference' do - let(:project) { double('project', to_reference: 'namespace1/project') } + let(:cross) { create(:project) } + + it 'returns a String reference to the object' do + expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}" + end + + it 'returns a String reference to the object' do + expect(range2.to_reference).to eq "#{full_sha_from}..#{full_sha_to}" + end + + it 'supports a cross-project reference' do + expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}" + end + end - before do - range.project = project + describe '#reference_link_text' do + let(:cross) { create(:project) } + + it 'returns a String reference to the object' do + expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}" end it 'returns a String reference to the object' do - expect(range.to_reference).to eq range.to_s + expect(range2.reference_link_text).to eq "#{sha_from}..#{sha_to}" end it 'supports a cross-project reference' do - cross = double('project') - expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}" + expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}" 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}" + expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}" end it 'returns the correct String for two-dot ranges' do - expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}" + expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}" end end @@ -60,11 +82,11 @@ describe CommitRange do end it 'includes the correct values for a three-dot range' do - expect(range.to_param).to eq({ from: sha_from, to: sha_to }) + expect(range.to_param).to eq({ from: full_sha_from, to: full_sha_to }) end it 'includes the correct values for a two-dot range' do - expect(range2.to_param).to eq({ from: sha_from + '^', to: sha_to }) + expect(range2.to_param).to eq({ from: full_sha_from + '^', to: full_sha_to }) end end @@ -79,64 +101,37 @@ describe CommitRange do end describe '#valid_commits?' do - context 'without a project' do - it 'returns nil' do - expect(range.valid_commits?).to be_nil + context 'with a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(true) end - end - - it 'accepts an optional project argument' do - project1 = double('project1').as_null_object - project2 = double('project2').as_null_object - - # project1 gets assigned through the accessor, but ignored when not given - # as an argument to `valid_commits?` - expect(project1).not_to receive(:present?) - range.project = project1 - - # project2 gets passed to `valid_commits?` - expect(project2).to receive(:present?).and_return(false) - range.valid_commits?(project2) - end - - context 'with a project' do - let(:project) { double('project', repository: double('repository')) } + it 'is false when `sha_from` is invalid' do + expect(project).to receive(:commit).with(sha_from).and_return(nil) + expect(project).to receive(:commit).with(sha_to).and_call_original - context 'with a valid repo' do - before do - expect(project).to receive(:valid_repo?).and_return(true) - range.project = project - end + expect(range).not_to be_valid_commits + end - it 'is false when `sha_from` is invalid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(false) - expect(project.repository).not_to receive(:commit).with(sha_to) - expect(range).not_to be_valid_commits - end + it 'is false when `sha_to` is invalid' do + expect(project).to receive(:commit).with(sha_from).and_call_original + expect(project).to receive(:commit).with(sha_to).and_return(nil) - it 'is false when `sha_to` is invalid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(true) - expect(project.repository).to receive(:commit).with(sha_to).and_return(false) - expect(range).not_to be_valid_commits - end + expect(range).not_to be_valid_commits + end - it 'is true when both `sha_from` and `sha_to` are valid' do - expect(project.repository).to receive(:commit).with(sha_from).and_return(true) - expect(project.repository).to receive(:commit).with(sha_to).and_return(true) - expect(range).to be_valid_commits - end + it 'is true when both `sha_from` and `sha_to` are valid' do + expect(range).to be_valid_commits end + end - context 'without a valid repo' do - before do - expect(project).to receive(:valid_repo?).and_return(false) - range.project = project - end + context 'without a valid repo' do + before do + expect(project).to receive(:valid_repo?).and_return(false) + end - it 'returns false' do - expect(range).not_to be_valid_commits - end + it 'returns false' do + expect(range).not_to be_valid_commits end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 90be93249511f5b8eef45d4b5d65587758c37553..974b52c1833ab9fd01ee366019eb167ec4e9c628 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -24,6 +24,17 @@ describe Commit do end end + describe '#reference_link_text' do + it 'returns a String reference to the object' do + expect(commit.reference_link_text).to eq commit.short_id + end + + it 'supports a cross-project reference' do + cross = double('project') + expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}" + end + end + describe '#title' do it "returns no_commit_message when safe_message is blank" do allow(commit).to receive(:safe_message).and_return('') @@ -77,14 +88,10 @@ eos let(:other_issue) { create :issue, project: other_project } it 'detects issues that this commit is marked as closing' do - allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid}") - expect(commit.closes_issues).to eq([issue]) - end - - it 'does not detect issues from other projects' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" - allow(commit).to receive(:safe_message).and_return("Fixes #{ext_ref}") - expect(commit.closes_issues).to be_empty + allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}") + expect(commit.closes_issues).to include(issue) + expect(commit.closes_issues).to include(other_issue) end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 97e5c270a59b994222f05f7067702a9665bcec0a..91e3bee13c1a987d28e22fd493ff8bebadbf8284 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -35,11 +35,24 @@ module FilterSpecHelper pipeline.call(body) end - def reference_pipeline_result(body, contexts = {}) + def reference_pipeline(contexts = {}) contexts.reverse_merge!(project: project) if defined?(project) - pipeline = HTML::Pipeline.new([described_class, Gitlab::Markdown::ReferenceGathererFilter], contexts) - pipeline.call(body) + filters = [ + Gitlab::Markdown::AutolinkFilter, + described_class, + Gitlab::Markdown::ReferenceGathererFilter + ] + + HTML::Pipeline.new(filters, contexts) + end + + def reference_pipeline_result(body, contexts = {}) + reference_pipeline(contexts).call(body) + end + + def reference_filter(html, contexts = {}) + reference_pipeline(contexts).to_document(html) end # Modify a String reference to make it invalid diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index bedc1a7f1db47960b8c8c53137782fdf9fbb5a5f..d6d3062a197a84ba7ef8ce33cdecf8c035fdc254 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -93,6 +93,10 @@ class MarkdownFeature end end + def urls + Gitlab::Application.routes.url_helpers + end + def raw_markdown markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb')) ERB.new(markdown).result(binding) diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 7500d0fdf80bbb0d7ce04d3cc384ba0cb7e1828d..7eadcd58c1fcb061489712c10cc59026a597fe61 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -71,7 +71,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3) + expect(actual).to have_selector('a.gfm.gfm-project_member', count: 4) end end @@ -80,7 +80,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-issue', count: 3) + expect(actual).to have_selector('a.gfm.gfm-issue', count: 6) end end @@ -89,7 +89,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3) + expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 6) expect(actual).to have_selector('em a.gfm-merge_request') end end @@ -99,7 +99,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2) + expect(actual).to have_selector('a.gfm.gfm-snippet', count: 5) end end @@ -108,7 +108,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2) + expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 5) end end @@ -117,7 +117,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-commit', count: 2) + expect(actual).to have_selector('a.gfm.gfm-commit', count: 5) end end @@ -126,7 +126,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-label', count: 3) + expect(actual).to have_selector('a.gfm.gfm-label', count: 4) end end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index 3bb568f4d494858c432f259cf37c20ef7465f535..33d2b14583c61b52de1bea228aae2d0ed61daf4f 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.commit } + let(:mentioned_commit) { project.commit("HEAD~1") } 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.commit } + let(:ext_commit) { ext_proj.commit("HEAD~2") } # Override to add known commits to the repository stub. let(:extra_commits) { [] } @@ -45,14 +45,11 @@ def common_mentionable_setup before do # Wire the project's repository to return the mentioned commit, and +nil+ # for any unrecognized commits. - commitmap = { - mentioned_commit.id => mentioned_commit - } - extra_commits.each { |c| commitmap[c.short_id] = c } - - allow(Project).to receive(:find).and_call_original - allow(Project).to receive(:find).with(project.id.to_s).and_return(project) - allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } + allow_any_instance_of(::Repository).to receive(:commit).and_call_original + allow_any_instance_of(::Repository).to receive(:commit).with(mentioned_commit.short_id).and_return(mentioned_commit) + extra_commits.each do |commit| + allow_any_instance_of(::Repository).to receive(:commit).with(commit.short_id).and_return(commit) + end set_mentionable_text.call(ref_string) end