Commit 701c2e9a authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rs-to_reference' into 'master'

Add to_reference method to referable models

Now there is a single source of information for which attribute a model uses to be referenced, and its special character.

See merge request !641
parents 3ed05b21 3cb6a338
...@@ -135,15 +135,25 @@ module GitlabMarkdownHelper ...@@ -135,15 +135,25 @@ module GitlabMarkdownHelper
end end
end end
# Returns the text necessary to reference `entity` across projects
#
# project - Project to reference
# entity - Object that responds to `to_reference`
#
# Examples:
#
# cross_project_reference(project, project.issues.first)
# # => 'namespace1/project1#123'
#
# cross_project_reference(project, project.merge_requests.first)
# # => 'namespace1/project1!345'
#
# Returns a String
def cross_project_reference(project, entity) def cross_project_reference(project, entity)
path = project.path_with_namespace if entity.respond_to?(:to_reference)
"#{project.to_reference}#{entity.to_reference}"
if entity.kind_of?(Issue)
[path, entity.iid].join('#')
elsif entity.kind_of?(MergeRequest)
[path, entity.iid].join('!')
else else
raise 'Not supported type' ''
end end
end end
end end
class Commit class Commit
include ActiveModel::Conversion
include StaticModel
extend ActiveModel::Naming extend ActiveModel::Naming
include ActiveModel::Conversion
include Mentionable include Mentionable
include Participable include Participable
include Referable
include StaticModel
attr_mentionable :safe_message attr_mentionable :safe_message
participant :author, :committer, :notes, :mentioned_users participant :author, :committer, :notes, :mentioned_users
...@@ -56,6 +58,34 @@ class Commit ...@@ -56,6 +58,34 @@ class Commit
@raw.id @raw.id
end end
def ==(other)
(self.class === other) && (raw == other.raw)
end
def self.reference_prefix
'@'
end
# Pattern used to extract commit references from text
#
# The SHA can be between 6 and 40 hex characters.
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{6,40})
}x
end
def to_reference(from_project = nil)
if cross_project_reference?(from_project)
"#{project.to_reference}@#{id}"
else
id
end
end
def diff_line_count def diff_line_count
@diff_line_count ||= Commit::diff_line_count(self.diffs) @diff_line_count ||= Commit::diff_line_count(self.diffs)
@diff_line_count @diff_line_count
...@@ -126,11 +156,6 @@ class Commit ...@@ -126,11 +156,6 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end end
# Mentionable override.
def gfm_reference
"commit #{id}"
end
def author def author
User.find_for_commit(author_email, author_name) User.find_for_commit(author_email, author_name)
end end
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
# #
class CommitRange class CommitRange
include ActiveModel::Conversion include ActiveModel::Conversion
include Referable
attr_reader :sha_from, :notation, :sha_to attr_reader :sha_from, :notation, :sha_to
...@@ -28,10 +29,24 @@ class CommitRange ...@@ -28,10 +29,24 @@ class CommitRange
# See `exclude_start?` # See `exclude_start?`
attr_reader :exclude_start attr_reader :exclude_start
# The beginning and ending SHA sums can be between 6 and 40 hex characters, # The beginning and ending SHAs can be between 6 and 40 hex characters, and
# and the range selection can be double- or triple-dot. # the range notation can be double- or triple-dot.
PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
def self.reference_prefix
'@'
end
# Pattern used to extract commit range references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{PATTERN})
}x
end
# Initialize a CommitRange # Initialize a CommitRange
# #
# range_string - The String commit range. # range_string - The String commit range.
...@@ -59,6 +74,17 @@ class CommitRange ...@@ -59,6 +74,17 @@ class CommitRange
"#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
end end
def to_reference(from_project = nil)
# Not using to_s because we want the full SHAs
reference = sha_from + notation + sha_to
if cross_project_reference?(from_project)
reference = project.to_reference + '@' + reference
end
reference
end
# Returns a String for use in a link's title attribute # Returns a String for use in a link's title attribute
def reference_title def reference_title
"Commits #{suffixed_sha_from} through #{sha_to}" "Commits #{suffixed_sha_from} through #{sha_to}"
......
...@@ -20,10 +20,15 @@ module Mentionable ...@@ -20,10 +20,15 @@ module Mentionable
end end
end end
# Generate a GFM back-reference that will construct a link back to this Mentionable when rendered. Must # Returns the text used as the body of a Note when this object is referenced
# be overridden if this model object can be referenced directly by GFM notation. #
def gfm_reference # By default this will be the class name and the result of calling
raise NotImplementedError.new("#{self.class} does not implement #gfm_reference") # `to_reference` on the object.
def gfm_reference(from_project = nil)
# "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase
"#{friendly_name} #{to_reference(from_project)}"
end end
# Construct a String that contains possible GFM references. # Construct a String that contains possible GFM references.
......
# == Referable concern
#
# Contains functionality related to making a model referable in Markdown, such
# as "#1", "!2", "~3", etc.
module Referable
extend ActiveSupport::Concern
# Returns the String necessary to reference this object in Markdown
#
# from_project - Refering Project object
#
# This should be overridden by the including class.
#
# Examples:
#
# Issue.first.to_reference # => "#1"
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
def to_reference(_from_project = nil)
''
end
module ClassMethods
# The character that prefixes the actual reference identifier
#
# This should be overridden by the including class.
#
# Examples:
#
# Issue.reference_prefix # => '#'
# MergeRequest.reference_prefix # => '!'
#
# Returns a String
def reference_prefix
''
end
# Regexp pattern used to match references to this object
#
# This must be overridden by the including class.
#
# Returns a Regexp
def reference_pattern
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
end
private
# Check if a reference is being done cross-project
#
# from_project - Refering Project object
def cross_project_reference?(from_project)
if self.is_a?(Project)
self != from_project
else
from_project && self.project && self.project != from_project
end
end
end
class ExternalIssue class ExternalIssue
include Referable
def initialize(issue_identifier, project) def initialize(issue_identifier, project)
@issue_identifier, @project = issue_identifier, project @issue_identifier, @project = issue_identifier, project
end end
...@@ -26,4 +28,13 @@ class ExternalIssue ...@@ -26,4 +28,13 @@ class ExternalIssue
def project def project
@project @project
end end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
%r{(?<issue>([A-Z\-]+-)\d+)}
end
def to_reference(_from_project = nil)
id
end
end end
...@@ -17,6 +17,8 @@ require 'carrierwave/orm/activerecord' ...@@ -17,6 +17,8 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Group < Namespace class Group < Namespace
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
has_many :users, through: :group_members has_many :users, through: :group_members
...@@ -36,6 +38,18 @@ class Group < Namespace ...@@ -36,6 +38,18 @@ class Group < Namespace
def sort(method) def sort(method)
order_by(method) order_by(method)
end end
def reference_prefix
User.reference_prefix
end
def reference_pattern
User.reference_pattern
end
end
def to_reference(_from_project = nil)
"#{self.class.reference_prefix}#{name}"
end end
def human_name def human_name
......
...@@ -21,10 +21,11 @@ require 'carrierwave/orm/activerecord' ...@@ -21,10 +21,11 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Issue < ActiveRecord::Base class Issue < ActiveRecord::Base
include Issuable
include InternalId include InternalId
include Taskable include Issuable
include Referable
include Sortable include Sortable
include Taskable
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
...@@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base ...@@ -53,10 +54,28 @@ class Issue < ActiveRecord::Base
attributes attributes
end end
# Mentionable overrides. def self.reference_prefix
'#'
end
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
if cross_project_reference?(from_project)
reference = project.to_reference + reference
end
def gfm_reference reference
"issue ##{iid}"
end end
# Reset issue events cache # Reset issue events cache
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
# #
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include Referable
DEFAULT_COLOR = '#428BCA' DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
...@@ -34,6 +36,45 @@ class Label < ActiveRecord::Base ...@@ -34,6 +36,45 @@ class Label < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
def self.reference_prefix
'~'
end
# Pattern used to extract label references from text
def self.reference_pattern
%r{
#{reference_prefix}
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
[A-Za-z0-9_-]+ | # String-based single-word label title, or
"[^&\?,]+" # String-based multi-word label surrounded in quotes
)
)
}x
end
# Returns the String necessary to reference this Label in Markdown
#
# format - Symbol format to use (default: :id, optional: :name)
#
# Note that its argument differs from other objects implementing Referable. If
# a non-Symbol argument is given (such as a Project), it will default to :id.
#
# Examples:
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(:name) # => "~\"bug\""
#
# Returns a String
def to_reference(format = :id)
if format == :name && !name.include?('"')
%(#{self.class.reference_prefix}"#{name}")
else
"#{self.class.reference_prefix}#{id}"
end
end
def open_issues_count def open_issues_count
issues.opened.count issues.opened.count
end end
......
...@@ -25,10 +25,11 @@ require Rails.root.join("app/models/commit") ...@@ -25,10 +25,11 @@ require Rails.root.join("app/models/commit")
require Rails.root.join("lib/static_model") require Rails.root.join("lib/static_model")
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include Issuable
include Taskable
include InternalId include InternalId
include Issuable
include Referable
include Sortable include Sortable
include Taskable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
...@@ -135,6 +136,30 @@ class MergeRequest < ActiveRecord::Base ...@@ -135,6 +136,30 @@ class MergeRequest < ActiveRecord::Base
scope :closed, -> { with_states(:closed, :merged) } scope :closed, -> { with_states(:closed, :merged) }
scope :declined, -> { with_states(:closed) } scope :declined, -> { with_states(:closed) }
def self.reference_prefix
'!'
end
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
}x
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
if cross_project_reference?(from_project)
reference = project.to_reference + reference
end
reference
end
def validate_branches def validate_branches
if target_project == source_project && target_branch == source_branch if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target" errors.add :branch_conflict, "You can not use same project/branch for source and target"
...@@ -289,11 +314,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -289,11 +314,6 @@ class MergeRequest < ActiveRecord::Base
end end
end end
# Mentionable override.
def gfm_reference
"merge request !#{iid}"
end
def target_project_path def target_project_path
if target_project if target_project
target_project.path_with_namespace target_project.path_with_namespace
......
...@@ -326,8 +326,8 @@ class Note < ActiveRecord::Base ...@@ -326,8 +326,8 @@ class Note < ActiveRecord::Base
end end
# Mentionable override. # Mentionable override.
def gfm_reference def gfm_reference(from_project = nil)
noteable.gfm_reference noteable.gfm_reference(from_project)
end end
# Mentionable override. # Mentionable override.
......
...@@ -33,11 +33,12 @@ require 'carrierwave/orm/activerecord' ...@@ -33,11 +33,12 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
include Sortable include Gitlab::ConfigHelper
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Gitlab::ConfigHelper
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
include Referable
include Sortable
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
extend Enumerize extend Enumerize
...@@ -247,6 +248,11 @@ class Project < ActiveRecord::Base ...@@ -247,6 +248,11 @@ class Project < ActiveRecord::Base
order_by(method) order_by(method)
end end
end end
def reference_pattern
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
end end
def team def team
...@@ -305,6 +311,10 @@ class Project < ActiveRecord::Base ...@@ -305,6 +311,10 @@ class Project < ActiveRecord::Base
path path
end end
def to_reference(_from_project = nil)
path_with_namespace
end
def web_url def web_url
[gitlab_config.url, path_with_namespace].join('/') [gitlab_config.url, path_with_namespace].join('/')
end end
......
...@@ -16,14 +16,16 @@ ...@@ -16,14 +16,16 @@
# #
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Sortable
include Linguist::BlobHelper
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Linguist::BlobHelper
include Participable include Participable
include Referable
include Sortable
default_value_for :visibility_level, Snippet::PRIVATE default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: "User" belongs_to :author, class_name: 'User'
belongs_to :project
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
...@@ -50,6 +52,30 @@ class Snippet < ActiveRecord::Base ...@@ -50,6 +52,30 @@ class Snippet < ActiveRecord::Base
participant :author, :notes participant :author, :notes
def self.reference_prefix
'$'
end
# Pattern used to extract `$123` snippet references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x
end
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{id}"
if cross_project_reference?(from_project)
reference = project.to_reference + reference
end
reference
end
def self.content_types def self.content_types
[ [
".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java", ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java",
......
...@@ -62,11 +62,13 @@ require 'carrierwave/orm/activerecord' ...@@ -62,11 +62,13 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class User < ActiveRecord::Base class User < ActiveRecord::Base
include Sortable
include Gitlab::ConfigHelper
include TokenAuthenticatable
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
include Gitlab::ConfigHelper
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Referable
include Sortable
include TokenAuthenticatable
default_value_for :admin, false default_value_for :admin, false
default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_group, gitlab_config.default_can_create_group
...@@ -247,6 +249,18 @@ class User < ActiveRecord::Base ...@@ -247,6 +249,18 @@ class User < ActiveRecord::Base
def build_user(attrs = {}) def build_user(attrs = {})
User.new(attrs) User.new(attrs)
end end
def reference_prefix
'@'
end
# Pattern used to extract `@user` user references from text
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})
}x
end
end end
# #
...@@ -257,6 +271,10 @@ class User < ActiveRecord::Base ...@@ -257,6 +271,10 @@ class User < ActiveRecord::Base
username username
end end
def to_reference(_from_project = nil)
"#{self.class.reference_prefix}#{username}"
end
def notification def notification
@notification ||= Notification.new(self) @notification ||= Notification.new(self)
end end
......
...@@ -10,7 +10,7 @@ class SystemNoteService ...@@ -10,7 +10,7 @@ class SystemNoteService
# author - User performing the change # author - User performing the change
# new_commits - Array of Commits added since last push # new_commits - Array of Commits added since last push
# existing_commits - Array of Commits added in a previous push # existing_commits - Array of Commits added in a previous push
# oldrev - TODO (rspeicher): I have no idea what this actually does # oldrev - Optional String SHA of a previous Commit
# #
# See new_commit_summary and existing_commit_summary. # See new_commit_summary and existing_commit_summary.
# #
...@@ -157,11 +157,11 @@ class SystemNoteService ...@@ -157,11 +157,11 @@ class SystemNoteService
# #
# Example Note text: # Example Note text:
# #
# "Mentioned in #1" # "mentioned in #1"
# #
# "Mentioned in !2" # "mentioned in !2"
# #
# "Mentioned in 54f7727c" # "mentioned in 54f7727c"
# #
# See cross_reference_note_content. # See cross_reference_note_content.
# #
...@@ -169,7 +169,7 @@ class SystemNoteService ...@@ -169,7 +169,7 @@ class SystemNoteService
def self.cross_reference(noteable, mentioner, author) def self.cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner) return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner) gfm_reference = mentioner.gfm_reference(noteable.project)
note_options = { note_options = {
project: noteable.project, project: noteable.project,
...@@ -200,12 +200,21 @@ class SystemNoteService ...@@ -200,12 +200,21 @@ class SystemNoteService
# #
# Returns Boolean # Returns Boolean
def self.cross_reference_disallowed?(noteable, mentioner) def self.cross_reference_disallowed?(noteable, mentioner)
return false unless MergeRequest === mentioner return false unless mentioner.is_a?(MergeRequest)
return false unless Commit === noteable return false unless noteable.is_a?(Commit)
mentioner.commits.include?(noteable) mentioner.commits.include?(noteable)
end end
# Check if a cross reference to a noteable from a mentioner already exists
#
# This method is used to prevent multiple notes being created for a mention
# when a issue is updated, for example.
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
#
# Returns Boolean
def self.cross_reference_exists?(noteable, mentioner) def self.cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type # Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class) notes = Note.system.where(noteable_type: noteable.class)
...@@ -217,7 +226,7 @@ class SystemNoteService ...@@ -217,7 +226,7 @@ class SystemNoteService
notes = notes.where(noteable_id: noteable.id) notes = notes.where(noteable_id: noteable.id)
end end
gfm_reference = mentioner_gfm_ref(noteable, mentioner, true) gfm_reference = mentioner.gfm_reference(noteable.project)
notes = notes.where(note: cross_reference_note_content(gfm_reference)) notes = notes.where(note: cross_reference_note_content(gfm_reference))
notes.count > 0 notes.count > 0
...@@ -229,39 +238,6 @@ class SystemNoteService ...@@ -229,39 +238,6 @@ class SystemNoteService
Note.create(args.merge(system: true)) Note.create(args.merge(system: true))
end end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def self.mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
# FIXME (rspeicher): This was breaking things.
# if mentioner.is_a?(Commit) && cross_reference
# return mentioner.gfm_reference.sub('commit ', 'commit %')
# end
full_gfm_reference(mentioner.project, noteable.project, mentioner)
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def self.full_gfm_reference(mentioning_project, noteable_project, mentioner)
if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
def self.cross_reference_note_prefix def self.cross_reference_note_prefix
'mentioned in ' 'mentioned in '
end end
...@@ -286,7 +262,7 @@ class SystemNoteService ...@@ -286,7 +262,7 @@ class SystemNoteService
# #
# noteable - MergeRequest object # noteable - MergeRequest object
# existing_commits - Array of existing Commit objects # existing_commits - Array of existing Commit objects
# oldrev - Optional String SHA of ... TODO (rspeicher): I have no idea what this actually does. # oldrev - Optional String SHA of a previous Commit
# #
# Examples: # Examples:
# #
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
- else - else
= f.submit 'Save', class: "btn-save btn" = f.submit 'Save', class: "btn-save btn"
- if @snippet.respond_to?(:project) - if @snippet.project_id
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
- else - else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
def closed_by_message(message) def closed_by_message(message)
return [] if message.nil? return [] if message.nil?
closing_statements = message.scan(ISSUE_CLOSING_REGEX). closing_statements = message.scan(ISSUE_CLOSING_REGEX).
map { |ref| ref[0] }.join(" ") map { |ref| ref[0] }.join(" ")
......
...@@ -19,7 +19,7 @@ module Gitlab ...@@ -19,7 +19,7 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(COMMIT_RANGE_PATTERN) do |match| text.gsub(CommitRange.reference_pattern) do |match|
yield match, $~[:commit_range], $~[:project] yield match, $~[:commit_range], $~[:project]
end end
end end
...@@ -30,13 +30,8 @@ module Gitlab ...@@ -30,13 +30,8 @@ module Gitlab
@commit_map = {} @commit_map = {}
end end
# Pattern used to extract commit range references from text
#
# This pattern supports cross-project references.
COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>#{CommitRange::PATTERN})/
def call def call
replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content| replace_text_nodes_matching(CommitRange.reference_pattern) do |content|
commit_range_link_filter(content) commit_range_link_filter(content)
end end
end end
......
...@@ -19,20 +19,13 @@ module Gitlab ...@@ -19,20 +19,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(COMMIT_PATTERN) do |match| text.gsub(Commit.reference_pattern) do |match|
yield match, $~[:commit], $~[:project] yield match, $~[:commit], $~[:project]
end end
end end
# Pattern used to extract commit references from text
#
# The SHA1 sum can be between 6 and 40 hex characters.
#
# This pattern supports cross-project references.
COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
def call def call
replace_text_nodes_matching(COMMIT_PATTERN) do |content| replace_text_nodes_matching(Commit.reference_pattern) do |content|
commit_link_filter(content) commit_link_filter(content)
end end
end end
......
...@@ -3,9 +3,6 @@ module Gitlab ...@@ -3,9 +3,6 @@ module Gitlab
# Common methods for ReferenceFilters that support an optional cross-project # Common methods for ReferenceFilters that support an optional cross-project
# reference. # reference.
module CrossProjectReference module CrossProjectReference
NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR
PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
# Given a cross-project reference string, get the Project record # Given a cross-project reference string, get the Project record
# #
# Defaults to value of `context[:project]` if: # Defaults to value of `context[:project]` if:
......
...@@ -16,19 +16,16 @@ module Gitlab ...@@ -16,19 +16,16 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(ISSUE_PATTERN) do |match| text.gsub(ExternalIssue.reference_pattern) do |match|
yield match, $~[:issue] yield match, $~[:issue]
end end
end end
# Pattern used to extract `JIRA-123` issue references from text
ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
def call def call
# Early return if the project isn't using an external tracker # Early return if the project isn't using an external tracker
return doc if project.nil? || project.default_issues_tracker? return doc if project.nil? || project.default_issues_tracker?
replace_text_nodes_matching(ISSUE_PATTERN) do |content| replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
issue_link_filter(content) issue_link_filter(content)
end end
end end
...@@ -51,7 +48,7 @@ module Gitlab ...@@ -51,7 +48,7 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{issue}</a>) class="#{klass}">#{match}</a>)
end end
end end
......
...@@ -20,18 +20,13 @@ module Gitlab ...@@ -20,18 +20,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(ISSUE_PATTERN) do |match| text.gsub(Issue.reference_pattern) do |match|
yield match, $~[:issue].to_i, $~[:project] yield match, $~[:issue].to_i, $~[:project]
end end
end end
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
def call def call
replace_text_nodes_matching(ISSUE_PATTERN) do |content| replace_text_nodes_matching(Issue.reference_pattern) do |content|
issue_link_filter(content) issue_link_filter(content)
end end
end end
...@@ -57,7 +52,7 @@ module Gitlab ...@@ -57,7 +52,7 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{project_ref}##{id}</a>) class="#{klass}">#{match}</a>)
else else
match match
end end
......
...@@ -15,26 +15,13 @@ module Gitlab ...@@ -15,26 +15,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(LABEL_PATTERN) do |match| text.gsub(Label.reference_pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name] yield match, $~[:label_id].to_i, $~[:label_name]
end end
end end
# Pattern used to extract label references from text
#
# TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
LABEL_PATTERN = %r{
~(
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
[A-Za-z0-9_-]+ | # String-based single-word label title
['"][^&\?,]+['"] # String-based multi-word label surrounded in quotes
)
)
}x
def call def call
replace_text_nodes_matching(LABEL_PATTERN) do |content| replace_text_nodes_matching(Label.reference_pattern) do |content|
label_link_filter(content) label_link_filter(content)
end end
end end
...@@ -85,8 +72,7 @@ module Gitlab ...@@ -85,8 +72,7 @@ module Gitlab
# Returns a Hash. # Returns a Hash.
def label_params(id, name) def label_params(id, name)
if name if name
# TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding. { name: name.tr('"', '') }
{ name: name.tr('\'"', '') }
else else
{ id: id } { id: id }
end end
......
...@@ -20,18 +20,13 @@ module Gitlab ...@@ -20,18 +20,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(MERGE_REQUEST_PATTERN) do |match| text.gsub(MergeRequest.reference_pattern) do |match|
yield match, $~[:merge_request].to_i, $~[:project] yield match, $~[:merge_request].to_i, $~[:project]
end end
end end
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
def call def call
replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content| replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
merge_request_link_filter(content) merge_request_link_filter(content)
end end
end end
...@@ -57,7 +52,7 @@ module Gitlab ...@@ -57,7 +52,7 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{project_ref}!#{id}</a>) class="#{klass}">#{match}</a>)
else else
match match
end end
......
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'html/pipeline' require 'html/pipeline/filter'
module Gitlab module Gitlab
module Markdown module Markdown
......
...@@ -20,18 +20,13 @@ module Gitlab ...@@ -20,18 +20,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(SNIPPET_PATTERN) do |match| text.gsub(Snippet.reference_pattern) do |match|
yield match, $~[:snippet].to_i, $~[:project] yield match, $~[:snippet].to_i, $~[:project]
end end
end end
# Pattern used to extract `$123` snippet references from text
#
# This pattern supports cross-project references.
SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
def call def call
replace_text_nodes_matching(SNIPPET_PATTERN) do |content| replace_text_nodes_matching(Snippet.reference_pattern) do |content|
snippet_link_filter(content) snippet_link_filter(content)
end end
end end
...@@ -57,7 +52,7 @@ module Gitlab ...@@ -57,7 +52,7 @@ module Gitlab
%(<a href="#{url}" %(<a href="#{url}"
title="#{title}" title="#{title}"
class="#{klass}">#{project_ref}$#{id}</a>) class="#{klass}">#{match}</a>)
else else
match match
end end
......
...@@ -16,16 +16,13 @@ module Gitlab ...@@ -16,16 +16,13 @@ module Gitlab
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def self.references_in(text)
text.gsub(USER_PATTERN) do |match| text.gsub(User.reference_pattern) do |match|
yield match, $~[:user] yield match, $~[:user]
end end
end end
# Pattern used to extract `@user` user references from text
USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
def call def call
replace_text_nodes_matching(USER_PATTERN) do |content| replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content) user_link_filter(content)
end end
end end
...@@ -68,7 +65,8 @@ module Gitlab ...@@ -68,7 +65,8 @@ module Gitlab
url = urls.namespace_project_url(project.namespace, project, url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path]) only_path: context[:only_path])
%(<a href="#{url}" class="#{link_class}">@all</a>) text = User.reference_prefix + 'all'
%(<a href="#{url}" class="#{link_class}">#{text}</a>)
end end
def link_to_namespace(namespace) def link_to_namespace(namespace)
...@@ -86,7 +84,8 @@ module Gitlab ...@@ -86,7 +84,8 @@ module Gitlab
url = urls.group_url(group, only_path: context[:only_path]) url = urls.group_url(group, only_path: context[:only_path])
%(<a href="#{url}" class="#{link_class}">@#{group}</a>) text = Group.reference_prefix + group
%(<a href="#{url}" class="#{link_class}">#{text}</a>)
end end
def link_to_user(user, namespace) def link_to_user(user, namespace)
...@@ -94,7 +93,8 @@ module Gitlab ...@@ -94,7 +93,8 @@ module Gitlab
url = urls.user_url(user, only_path: context[:only_path]) url = urls.user_url(user, only_path: context[:only_path])
%(<a href="#{url}" class="#{link_class}">@#{user}</a>) text = User.reference_prefix + user
%(<a href="#{url}" class="#{link_class}">#{text}</a>)
end end
def user_can_reference_group?(group) def user_can_reference_group?(group)
......
...@@ -11,7 +11,7 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -11,7 +11,7 @@ describe "GitLab Flavored Markdown", feature: true do
end end
before do before do
Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details") Commit.any_instance.stub(title: "fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
end end
let(:commit) { project.commit } let(:commit) { project.commit }
...@@ -25,25 +25,25 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -25,25 +25,25 @@ describe "GitLab Flavored Markdown", feature: true do
it "should render title in commits#index" do it "should render title in commits#index" do
visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
it "should render title in commits#show" do it "should render title in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit) visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
it "should render description in commits#show" do it "should render description in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit) visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link("@#{fred.username}") expect(page).to have_link(fred.to_reference)
end end
it "should render title in repositories#branches" do it "should render title in repositories#branches" do
visit namespace_project_branches_path(project.namespace, project) visit namespace_project_branches_path(project.namespace, project)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
end end
...@@ -57,20 +57,20 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -57,20 +57,20 @@ describe "GitLab Flavored Markdown", feature: true do
author: @user, author: @user,
assignee: @user, assignee: @user,
project: project, project: project,
title: "fix ##{@other_issue.iid}", title: "fix #{@other_issue.to_reference}",
description: "ask @#{fred.username} for details") description: "ask #{fred.to_reference} for details")
end end
it "should render subject in issues#index" do it "should render subject in issues#index" do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
expect(page).to have_link("##{@other_issue.iid}") expect(page).to have_link(@other_issue.to_reference)
end end
it "should render subject in issues#show" do it "should render subject in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue) visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link("##{@other_issue.iid}") expect(page).to have_link(@other_issue.to_reference)
end end
it "should render details in issues#show" do it "should render details in issues#show" do
...@@ -83,19 +83,19 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -83,19 +83,19 @@ describe "GitLab Flavored Markdown", feature: true do
describe "for merge requests" do describe "for merge requests" do
before do before do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix ##{issue.iid}") @merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
end end
it "should render title in merge_requests#index" do it "should render title in merge_requests#index" do
visit namespace_project_merge_requests_path(project.namespace, project) visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
it "should render title in merge_requests#show" do it "should render title in merge_requests#show" do
visit namespace_project_merge_request_path(project.namespace, project, @merge_request) visit namespace_project_merge_request_path(project.namespace, project, @merge_request)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
end end
...@@ -104,26 +104,26 @@ describe "GitLab Flavored Markdown", feature: true do ...@@ -104,26 +104,26 @@ describe "GitLab Flavored Markdown", feature: true do
before do before do
@milestone = create(:milestone, @milestone = create(:milestone,
project: project, project: project,
title: "fix ##{issue.iid}", title: "fix #{issue.to_reference}",
description: "ask @#{fred.username} for details") description: "ask #{fred.to_reference} for details")
end end
it "should render title in milestones#index" do it "should render title in milestones#index" do
visit namespace_project_milestones_path(project.namespace, project) visit namespace_project_milestones_path(project.namespace, project)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
it "should render title in milestones#show" do it "should render title in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone) visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link("##{issue.iid}") expect(page).to have_link(issue.to_reference)
end end
it "should render description in milestones#show" do it "should render description in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone) visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link("@#{fred.username}") expect(page).to have_link(fred.to_reference)
end end
end end
end end
...@@ -66,6 +66,10 @@ describe 'GitLab Markdown' do ...@@ -66,6 +66,10 @@ describe 'GitLab Markdown' do
@doc.at_css("##{id}").parent.next_element @doc.at_css("##{id}").parent.next_element
end end
# Sometimes it can be useful to see the parsed output of the Markdown document
# for debugging. Uncomment this block to write the output to
# tmp/capybara/markdown_spec.html.
#
# it 'writes to a file' do # it 'writes to a file' do
# File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file|
# file.puts @md # file.puts @md
...@@ -344,13 +348,13 @@ class MarkdownFeature ...@@ -344,13 +348,13 @@ class MarkdownFeature
end end
def commit def commit
@commit ||= project.repository.commit @commit ||= project.commit
end end
def commit_range def commit_range
unless @commit_range unless @commit_range
commit2 = project.repository.commit('HEAD~3') commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}") @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project)
end end
@commit_range @commit_range
...@@ -376,11 +380,6 @@ class MarkdownFeature ...@@ -376,11 +380,6 @@ class MarkdownFeature
@xproject @xproject
end end
# Shortcut to "cross-reference/project"
def xref
xproject.path_with_namespace
end
def xissue def xissue
@xissue ||= create(:issue, project: xproject) @xissue ||= create(:issue, project: xproject)
end end
...@@ -394,13 +393,13 @@ class MarkdownFeature ...@@ -394,13 +393,13 @@ class MarkdownFeature
end end
def xcommit def xcommit
@xcommit ||= xproject.repository.commit @xcommit ||= xproject.commit
end end
def xcommit_range def xcommit_range
unless @xcommit_range unless @xcommit_range
xcommit2 = xproject.repository.commit('HEAD~2') xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}") @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end end
@xcommit_range @xcommit_range
......
...@@ -127,61 +127,61 @@ But it shouldn't autolink text inside certain tags: ...@@ -127,61 +127,61 @@ But it shouldn't autolink text inside certain tags:
- <a>http://about.gitlab.com/</a> - <a>http://about.gitlab.com/</a>
- <kbd>http://about.gitlab.com/</kbd> - <kbd>http://about.gitlab.com/</kbd>
### Reference Filters (e.g., #<%= issue.iid %>) ### Reference Filters (e.g., <%= issue.to_reference %>)
References should be parseable even inside _!<%= merge_request.iid %>_ emphasis. References should be parseable even inside _<%= merge_request.to_reference %>_ emphasis.
#### UserReferenceFilter #### UserReferenceFilter
- All: @all - All: @all
- User: @<%= user.username %> - User: <%= user.to_reference %>
- Group: @<%= group.name %> - Group: <%= group.to_reference %>
- Ignores invalid: @fake_user - Ignores invalid: <%= User.reference_prefix %>fake_user
- Ignored in code: `@<%= user.username %>` - Ignored in code: `<%= user.to_reference %>`
- Ignored in links: [Link to @<%= user.username %>](#user-link) - Ignored in links: [Link to <%= user.to_reference %>](#user-link)
#### IssueReferenceFilter #### IssueReferenceFilter
- Issue: #<%= issue.iid %> - Issue: <%= issue.to_reference %>
- Issue in another project: <%= xref %>#<%= xissue.iid %> - Issue in another project: <%= xissue.to_reference(project) %>
- Ignored in code: `#<%= issue.iid %>` - Ignored in code: `<%= issue.to_reference %>`
- Ignored in links: [Link to #<%= issue.iid %>](#issue-link) - Ignored in links: [Link to <%= issue.to_reference %>](#issue-link)
#### MergeRequestReferenceFilter #### MergeRequestReferenceFilter
- Merge request: !<%= merge_request.iid %> - Merge request: <%= merge_request.to_reference %>
- Merge request in another project: <%= xref %>!<%= xmerge_request.iid %> - Merge request in another project: <%= xmerge_request.to_reference(project) %>
- Ignored in code: `!<%= merge_request.iid %>` - Ignored in code: `<%= merge_request.to_reference %>`
- Ignored in links: [Link to !<%= merge_request.iid %>](#merge-request-link) - Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link)
#### SnippetReferenceFilter #### SnippetReferenceFilter
- Snippet: $<%= snippet.id %> - Snippet: <%= snippet.to_reference %>
- Snippet in another project: <%= xref %>$<%= xsnippet.id %> - Snippet in another project: <%= xsnippet.to_reference(project) %>
- Ignored in code: `$<%= snippet.id %>` - Ignored in code: `<%= snippet.to_reference %>`
- Ignored in links: [Link to $<%= snippet.id %>](#snippet-link) - Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link)
#### CommitRangeReferenceFilter #### CommitRangeReferenceFilter
- Range: <%= commit_range %> - Range: <%= commit_range.to_reference %>
- Range in another project: <%= xref %>@<%= xcommit_range %> - Range in another project: <%= xcommit_range.to_reference(project) %>
- Ignored in code: `<%= commit_range %>` - Ignored in code: `<%= commit_range.to_reference %>`
- Ignored in links: [Link to <%= commit_range %>](#commit-range-link) - Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link)
#### CommitReferenceFilter #### CommitReferenceFilter
- Commit: <%= commit.id %> - Commit: <%= commit.to_reference %>
- Commit in another project: <%= xref %>@<%= xcommit.id %> - Commit in another project: <%= xcommit.to_reference(project) %>
- Ignored in code: `<%= commit.id %>` - Ignored in code: `<%= commit.to_reference %>`
- Ignored in links: [Link to <%= commit.id %>](#commit-link) - Ignored in links: [Link to <%= commit.to_reference %>](#commit-link)
#### LabelReferenceFilter #### LabelReferenceFilter
- Label by ID: ~<%= simple_label.id %> - Label by ID: <%= simple_label.to_reference %>
- Label by name: ~<%= simple_label.name %> - Label by name: <%= Label.reference_prefix %><%= simple_label.name %>
- Label by name in quotes: ~"<%= label.name %>" - Label by name in quotes: <%= label.to_reference(:name) %>
- Ignored in code: `~<%= simple_label.name %>` - Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to ~<%= simple_label.id %>](#label-link) - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
### Task Lists ### Task Lists
......
...@@ -26,7 +26,7 @@ describe GitlabMarkdownHelper do ...@@ -26,7 +26,7 @@ describe GitlabMarkdownHelper do
end end
describe "referencing multiple objects" do describe "referencing multiple objects" do
let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
it "should link to the merge request" do it "should link to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request) expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
...@@ -50,7 +50,7 @@ describe GitlabMarkdownHelper do ...@@ -50,7 +50,7 @@ describe GitlabMarkdownHelper do
let(:issues) { create_list(:issue, 2, project: project) } let(:issues) { create_list(:issue, 2, project: project) }
it 'should handle references nested in links with all the text' do it 'should handle references nested in links with all the text' do
actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual) doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup # Make sure we didn't create invalid markup
...@@ -63,7 +63,7 @@ describe GitlabMarkdownHelper do ...@@ -63,7 +63,7 @@ describe GitlabMarkdownHelper do
# First issue link # First issue link
expect(doc.css('a')[1].attr('href')). expect(doc.css('a')[1].attr('href')).
to eq namespace_project_issue_path(project.namespace, project, issues[0]) to eq namespace_project_issue_path(project.namespace, project, issues[0])
expect(doc.css('a')[1].text).to eq "##{issues[0].iid}" expect(doc.css('a')[1].text).to eq issues[0].to_reference
# Internal commit link # Internal commit link
expect(doc.css('a')[2].attr('href')).to eq commit_path expect(doc.css('a')[2].attr('href')).to eq commit_path
...@@ -72,7 +72,7 @@ describe GitlabMarkdownHelper do ...@@ -72,7 +72,7 @@ describe GitlabMarkdownHelper do
# Second issue link # Second issue link
expect(doc.css('a')[3].attr('href')). expect(doc.css('a')[3].attr('href')).
to eq namespace_project_issue_path(project.namespace, project, issues[1]) to eq namespace_project_issue_path(project.namespace, project, issues[1])
expect(doc.css('a')[3].text).to eq "##{issues[1].iid}" expect(doc.css('a')[3].text).to eq issues[1].to_reference
# Trailing commit link # Trailing commit link
expect(doc.css('a')[4].attr('href')).to eq commit_path expect(doc.css('a')[4].attr('href')).to eq commit_path
...@@ -90,7 +90,7 @@ describe GitlabMarkdownHelper do ...@@ -90,7 +90,7 @@ describe GitlabMarkdownHelper do
end end
it "escapes HTML passed in as the body" do it "escapes HTML passed in as the body" do
actual = "This is a <h1>test</h1> - see ##{issues[0].iid}" actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
expect(link_to_gfm(actual, commit_path)). expect(link_to_gfm(actual, commit_path)).
to match('&lt;h1&gt;test&lt;/h1&gt;') to match('&lt;h1&gt;test&lt;/h1&gt;')
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ClosingIssueExtractor do describe Gitlab::ClosingIssueExtractor do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:iid1) { issue.iid } let(:reference) { issue.to_reference }
subject { described_class.new(project, project.creator) } subject { described_class.new(project, project.creator) }
describe "#closed_by_message" do describe "#closed_by_message" do
context 'with a single reference' do context 'with a single reference' do
it do it do
message = "Awesome commit (Closes ##{iid1})" message = "Awesome commit (Closes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Awesome commit (closes ##{iid1})" message = "Awesome commit (closes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Closed ##{iid1}" message = "Closed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "closed ##{iid1}" message = "closed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Closing ##{iid1}" message = "Closing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "closing ##{iid1}" message = "closing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Close ##{iid1}" message = "Close #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "close ##{iid1}" message = "close #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Awesome commit (Fixes ##{iid1})" message = "Awesome commit (Fixes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Awesome commit (fixes ##{iid1})" message = "Awesome commit (fixes #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Fixed ##{iid1}" message = "Fixed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "fixed ##{iid1}" message = "fixed #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Fixing ##{iid1}" message = "Fixing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "fixing ##{iid1}" message = "fixing #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Fix ##{iid1}" message = "Fix #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "fix ##{iid1}" message = "fix #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Awesome commit (Resolves ##{iid1})" message = "Awesome commit (Resolves #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Awesome commit (resolves ##{iid1})" message = "Awesome commit (resolves #{reference})"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Resolved ##{iid1}" message = "Resolved #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "resolved ##{iid1}" message = "resolved #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Resolving ##{iid1}" message = "Resolving #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "resolving ##{iid1}" message = "resolving #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "Resolve ##{iid1}" message = "Resolve #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
it do it do
message = "resolve ##{iid1}" message = "resolve #{reference}"
expect(subject.closed_by_message(message)).to eq([issue]) expect(subject.closed_by_message(message)).to eq([issue])
end end
end end
...@@ -133,40 +133,40 @@ describe Gitlab::ClosingIssueExtractor do ...@@ -133,40 +133,40 @@ describe Gitlab::ClosingIssueExtractor do
context 'with multiple references' do context 'with multiple references' do
let(:other_issue) { create(:issue, project: project) } let(:other_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) }
let(:iid2) { other_issue.iid } let(:reference2) { other_issue.to_reference }
let(:iid3) { third_issue.iid } let(:reference3) { third_issue.to_reference }
it 'fetches issues in single line message' do it 'fetches issues in single line message' do
message = "Closes ##{iid1} and fix ##{iid2}" message = "Closes #{reference} and fix #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to eq([issue, other_issue])
end end
it 'fetches comma-separated issues references in single line message' do it 'fetches comma-separated issues references in single line message' do
message = "Closes ##{iid1}, closes ##{iid2}" message = "Closes #{reference}, closes #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to eq([issue, other_issue])
end end
it 'fetches comma-separated issues numbers in single line message' do it 'fetches comma-separated issues numbers in single line message' do
message = "Closes ##{iid1}, ##{iid2} and ##{iid3}" message = "Closes #{reference}, #{reference2} and #{reference3}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue]) to eq([issue, other_issue, third_issue])
end end
it 'fetches issues in multi-line message' do it 'fetches issues in multi-line message' do
message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}" message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to eq([issue, other_issue])
end end
it 'fetches issues in hybrid message' do it 'fetches issues in hybrid message' do
message = "Awesome commit (closes ##{iid1})\n"\ message = "Awesome commit (closes #{reference})\n"\
"Also fixing issues ##{iid2}, ##{iid3} and #4" "Also fixing issues #{reference2}, #{reference3} and #4"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue]) to eq([issue, other_issue, third_issue])
......
...@@ -8,34 +8,36 @@ module Gitlab::Markdown ...@@ -8,34 +8,36 @@ module Gitlab::Markdown
let(:commit1) { project.commit } let(:commit1) { project.commit }
let(:commit2) { project.commit("HEAD~2") } let(:commit2) { project.commit("HEAD~2") }
let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") }
let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Commit Range 1c002d..d200c1', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>" exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
context 'internal reference' do context 'internal reference' do
let(:reference) { "#{commit1.id}...#{commit2.id}" } let(:reference) { range.to_reference }
let(:reference2) { "#{commit1.id}..#{commit2.id}" } let(:reference2) { range2.to_reference }
it 'links to a valid two-dot reference' do it 'links to a valid two-dot reference' do
doc = filter("See #{reference2}") doc = filter("See #{reference2}")
expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id) to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
end end
it 'links to a valid three-dot reference' do it 'links to a valid three-dot reference' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id) to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
end end
it 'links to a valid short ID' do it 'links to a valid short ID' do
...@@ -51,7 +53,7 @@ module Gitlab::Markdown ...@@ -51,7 +53,7 @@ module Gitlab::Markdown
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("See (#{reference}.)") doc = filter("See (#{reference}.)")
exp = Regexp.escape("#{commit1.short_id}...#{commit2.short_id}") exp = Regexp.escape(range.to_s)
expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end end
...@@ -65,7 +67,7 @@ module Gitlab::Markdown ...@@ -65,7 +67,7 @@ module Gitlab::Markdown
it 'includes a title attribute' do it 'includes a title attribute' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}" expect(doc.css('a').first.attr('title')).to eq range.reference_title
end end
it 'includes default classes' do it 'includes default classes' do
...@@ -95,9 +97,11 @@ module Gitlab::Markdown ...@@ -95,9 +97,11 @@ module Gitlab::Markdown
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, namespace: namespace) }
let(:commit1) { project.commit } let(:reference) { range.to_reference(project) }
let(:commit2) { project.commit("HEAD~2") }
let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } before do
range.project = project2
end
context 'when user can access reference' do context 'when user can access reference' do
before { allow_cross_reference! } before { allow_cross_reference! }
...@@ -106,21 +110,21 @@ module Gitlab::Markdown ...@@ -106,21 +110,21 @@ module Gitlab::Markdown
doc = filter("See #{reference}") doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')). expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id) to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
end end
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)") doc = filter("Fixed (#{reference}.)")
exp = Regexp.escape("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}") exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}")
expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end end
it 'ignores invalid commit IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}" exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
......
...@@ -8,8 +8,7 @@ module Gitlab::Markdown ...@@ -8,8 +8,7 @@ module Gitlab::Markdown
let(:commit) { project.commit } let(:commit) { project.commit }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Commit 1c002d', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
...@@ -47,10 +46,11 @@ module Gitlab::Markdown ...@@ -47,10 +46,11 @@ module Gitlab::Markdown
end end
it 'ignores invalid commit IDs' do it 'ignores invalid commit IDs' do
exp = act = "See #{reference.reverse}" invalid = invalidate_reference(reference)
exp = act = "See #{invalid}"
expect(project).to receive(:valid_repo?).and_return(true) expect(project).to receive(:valid_repo?).and_return(true)
expect(project.repository).to receive(:commit).with(reference.reverse) expect(project.repository).to receive(:commit).with(invalid)
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -93,8 +93,8 @@ module Gitlab::Markdown ...@@ -93,8 +93,8 @@ module Gitlab::Markdown
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, namespace: namespace) }
let(:commit) { project.commit } let(:commit) { project2.commit }
let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" } let(:reference) { commit.to_reference(project) }
context 'when user can access reference' do context 'when user can access reference' do
before { allow_cross_reference! } before { allow_cross_reference! }
...@@ -109,12 +109,12 @@ module Gitlab::Markdown ...@@ -109,12 +109,12 @@ module Gitlab::Markdown
it 'links with adjacent text' do it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)") doc = filter("Fixed (#{reference}.)")
exp = Regexp.escape(project2.path_with_namespace) exp = Regexp.escape(project2.to_reference)
expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
end end
it 'ignores invalid commit IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" exp = act = "Committed #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
......
...@@ -9,19 +9,18 @@ module Gitlab::Markdown ...@@ -9,19 +9,18 @@ module Gitlab::Markdown
end end
let(:project) { create(:jira_project) } let(:project) { create(:jira_project) }
let(:issue) { double('issue', iid: 123) }
context 'JIRA issue references' do context 'JIRA issue references' do
let(:reference) { "JIRA-#{issue.iid}" } let(:issue) { ExternalIssue.new('JIRA-123', project) }
let(:reference) { issue.to_reference }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Issue JIRA-123', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>" exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
...@@ -33,13 +32,6 @@ module Gitlab::Markdown ...@@ -33,13 +32,6 @@ module Gitlab::Markdown
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
%w(pre code a style).each do |elem|
it "ignores references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
it 'links to a valid reference' do it 'links to a valid reference' do
doc = filter("Issue #{reference}") doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('href')) expect(doc.css('a').first.attr('href'))
......
...@@ -12,24 +12,23 @@ module Gitlab::Markdown ...@@ -12,24 +12,23 @@ module Gitlab::Markdown
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Issue #123', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>" exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
context 'internal reference' do context 'internal reference' do
let(:reference) { "##{issue.iid}" } let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do it 'ignores valid references when using non-default tracker' do
expect(project).to receive(:get_issue).with(issue.iid).and_return(nil) expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
exp = act = "Issue ##{issue.iid}" exp = act = "Issue #{reference}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -46,9 +45,9 @@ module Gitlab::Markdown ...@@ -46,9 +45,9 @@ module Gitlab::Markdown
end end
it 'ignores invalid issue IDs' do it 'ignores invalid issue IDs' do
exp = act = "Fixed ##{issue.iid + 1}" invalid = invalidate_reference(reference)
exp = act = "Fixed #{invalid}"
expect(project).to receive(:get_issue).with(issue.iid + 1).and_return(nil)
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -92,7 +91,7 @@ module Gitlab::Markdown ...@@ -92,7 +91,7 @@ module Gitlab::Markdown
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) } let(:project2) { create(:empty_project, namespace: namespace) }
let(:issue) { create(:issue, project: project2) } let(:issue) { create(:issue, project: project2) }
let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } let(:reference) { issue.to_reference(project) }
context 'when user can access reference' do context 'when user can access reference' do
before { allow_cross_reference! } before { allow_cross_reference! }
...@@ -101,7 +100,7 @@ module Gitlab::Markdown ...@@ -101,7 +100,7 @@ module Gitlab::Markdown
expect_any_instance_of(Project).to receive(:get_issue). expect_any_instance_of(Project).to receive(:get_issue).
with(issue.iid).and_return(nil) with(issue.iid).and_return(nil)
exp = act = "Issue ##{issue.iid}" exp = act = "Issue #{reference}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -118,7 +117,7 @@ module Gitlab::Markdown ...@@ -118,7 +117,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid issue IDs on the referenced project' do it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}" exp = act = "Fixed #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
......
...@@ -7,11 +7,10 @@ module Gitlab::Markdown ...@@ -7,11 +7,10 @@ module Gitlab::Markdown
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let(:reference) { "~#{label.id}" } let(:reference) { label.to_reference }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Label ~123', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
...@@ -36,7 +35,7 @@ module Gitlab::Markdown ...@@ -36,7 +35,7 @@ module Gitlab::Markdown
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true) expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end end
it 'adds to the results hash' do it 'adds to the results hash' do
...@@ -70,7 +69,7 @@ module Gitlab::Markdown ...@@ -70,7 +69,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid label IDs' do it 'ignores invalid label IDs' do
exp = act = "Label ~#{label.id + 1}" exp = act = "Label #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -78,7 +77,7 @@ module Gitlab::Markdown ...@@ -78,7 +77,7 @@ module Gitlab::Markdown
context 'String-based single-word references' do context 'String-based single-word references' do
let(:label) { create(:label, name: 'gfm', project: project) } let(:label) { create(:label, name: 'gfm', project: project) }
let(:reference) { "~#{label.name}" } let(:reference) { "#{Label.reference_prefix}#{label.name}" }
it 'links to a valid reference' do it 'links to a valid reference' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
...@@ -94,59 +93,33 @@ module Gitlab::Markdown ...@@ -94,59 +93,33 @@ module Gitlab::Markdown
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
exp = act = "Label ~#{label.name.reverse}" exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
context 'String-based multi-word references in quotes' do context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) } let(:label) { create(:label, name: 'gfm references', project: project) }
let(:reference) { label.to_reference(:name) }
context 'in single quotes' do it 'links to a valid reference' do
let(:reference) { "~'#{label.name}'" } doc = filter("See #{reference}")
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = "Label ~'#{label.name.reverse}'"
expect(filter(act).to_html).to eq exp expect(doc.css('a').first.attr('href')).to eq urls.
end namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm references'
end end
context 'in double quotes' do it 'links with adjacent text' do
let(:reference) { %(~"#{label.name}") } doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
it 'links to a valid reference' do end
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do it 'ignores invalid label names' do
exp = act = %(Label ~"#{label.name.reverse}") exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end
end end
end end
......
...@@ -8,19 +8,18 @@ module Gitlab::Markdown ...@@ -8,19 +8,18 @@ module Gitlab::Markdown
let(:merge) { create(:merge_request, source_project: project) } let(:merge) { create(:merge_request, source_project: project) }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('MergeRequest !123', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>" exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
context 'internal reference' do context 'internal reference' do
let(:reference) { "!#{merge.iid}" } let(:reference) { merge.to_reference }
it 'links to a valid reference' do it 'links to a valid reference' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
...@@ -35,7 +34,7 @@ module Gitlab::Markdown ...@@ -35,7 +34,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid merge IDs' do it 'ignores invalid merge IDs' do
exp = act = "Merge !#{merge.iid + 1}" exp = act = "Merge #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -80,7 +79,7 @@ module Gitlab::Markdown ...@@ -80,7 +79,7 @@ module Gitlab::Markdown
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, namespace: namespace) }
let(:merge) { create(:merge_request, source_project: project2) } let(:merge) { create(:merge_request, source_project: project2) }
let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } let(:reference) { merge.to_reference(project) }
context 'when user can access reference' do context 'when user can access reference' do
before { allow_cross_reference! } before { allow_cross_reference! }
...@@ -99,7 +98,7 @@ module Gitlab::Markdown ...@@ -99,7 +98,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid merge IDs on the referenced project' do it 'ignores invalid merge IDs on the referenced project' do
exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}" exp = act = "Merge #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
......
...@@ -6,11 +6,10 @@ module Gitlab::Markdown ...@@ -6,11 +6,10 @@ module Gitlab::Markdown
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:snippet) { create(:project_snippet, project: project) } let(:snippet) { create(:project_snippet, project: project) }
let(:reference) { "$#{snippet.id}" } let(:reference) { snippet.to_reference }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Snippet $123', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
...@@ -34,7 +33,7 @@ module Gitlab::Markdown ...@@ -34,7 +33,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid snippet IDs' do it 'ignores invalid snippet IDs' do
exp = act = "Snippet $#{snippet.id + 1}" exp = act = "Snippet #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
...@@ -79,7 +78,7 @@ module Gitlab::Markdown ...@@ -79,7 +78,7 @@ module Gitlab::Markdown
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) } let(:project2) { create(:empty_project, namespace: namespace) }
let(:snippet) { create(:project_snippet, project: project2) } let(:snippet) { create(:project_snippet, project: project2) }
let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } let(:reference) { snippet.to_reference(project) }
context 'when user can access reference' do context 'when user can access reference' do
before { allow_cross_reference! } before { allow_cross_reference! }
...@@ -97,7 +96,7 @@ module Gitlab::Markdown ...@@ -97,7 +96,7 @@ module Gitlab::Markdown
end end
it 'ignores invalid snippet IDs on the referenced project' do it 'ignores invalid snippet IDs on the referenced project' do
exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}" exp = act = "See #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
......
...@@ -4,65 +4,63 @@ module Gitlab::Markdown ...@@ -4,65 +4,63 @@ module Gitlab::Markdown
describe UserReferenceFilter do describe UserReferenceFilter do
include ReferenceFilterSpecHelper include ReferenceFilterSpecHelper
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:reference) { user.to_reference }
it 'requires project context' do it 'requires project context' do
expect { described_class.call('Example @mention', {}) }. expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
to raise_error(ArgumentError, /:project/)
end end
it 'ignores invalid users' do it 'ignores invalid users' do
exp = act = 'Hey @somebody' exp = act = "Hey #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq(exp) expect(filter(act).to_html).to eq(exp)
end end
%w(pre code a style).each do |elem| %w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>" exp = act = "<#{elem}>Hey #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp expect(filter(act).to_html).to eq exp
end end
end end
context 'mentioning @all' do context 'mentioning @all' do
let(:reference) { User.reference_prefix + 'all' }
before do before do
project.team << [project.creator, :developer] project.team << [project.creator, :developer]
end end
it 'supports a special @all mention' do it 'supports a special @all mention' do
doc = filter("Hey @all") doc = filter("Hey #{reference}")
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href')) expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project) .to eq urls.namespace_project_url(project.namespace, project)
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result('Hey @all') result = pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [project.creator] expect(result[:references][:user]).to eq [project.creator]
end end
end end
context 'mentioning a user' do context 'mentioning a user' do
let(:reference) { "@#{user.username}" }
it 'links to a User' do it 'links to a User' do
doc = filter("Hey #{reference}") doc = filter("Hey #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end end
# TODO (rspeicher): This test might be overkill
it 'links to a User with a period' do it 'links to a User with a period' do
user = create(:user, name: 'alphA.Beta') user = create(:user, name: 'alphA.Beta')
doc = filter("Hey @#{user.username}") doc = filter("Hey #{user.to_reference}")
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
# TODO (rspeicher): This test might be overkill
it 'links to a User with an underscore' do it 'links to a User with an underscore' do
user = create(:user, name: 'ping_pong_king') user = create(:user, name: 'ping_pong_king')
doc = filter("Hey @#{user.username}") doc = filter("Hey #{user.to_reference}")
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
...@@ -73,10 +71,9 @@ module Gitlab::Markdown ...@@ -73,10 +71,9 @@ module Gitlab::Markdown
end end
context 'mentioning a group' do context 'mentioning a group' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:reference) { group.to_reference }
let(:reference) { "@#{group.name}" }
context 'that the current user can read' do context 'that the current user can read' do
before do before do
...@@ -108,23 +105,23 @@ module Gitlab::Markdown ...@@ -108,23 +105,23 @@ module Gitlab::Markdown
end end
it 'links with adjacent text' do it 'links with adjacent text' do
skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.' skip "TODO (rspeicher): Re-enable when usernames can't end in periods."
doc = filter("Mention me (@#{user.username}.)") doc = filter("Mention me (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
end end
it 'includes default classes' do it 'includes default classes' do
doc = filter("Hey @#{user.username}") doc = filter("Hey #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
end end
it 'includes an optional custom class' do it 'includes an optional custom class' do
doc = filter("Hey @#{user.username}", reference_class: 'custom') doc = filter("Hey #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom' expect(doc.css('a').first.attr('class')).to include 'custom'
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
doc = filter("Hey @#{user.username}", only_path: true) doc = filter("Hey #{reference}", only_path: true)
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://) expect(link).not_to match %r(https?://)
......
...@@ -20,7 +20,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -20,7 +20,7 @@ describe Gitlab::ReferenceExtractor do
@i0 = create(:issue, project: project) @i0 = create(:issue, project: project)
@i1 = create(:issue, project: project) @i1 = create(:issue, project: project)
subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.") subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.")
expect(subject.issues).to eq([@i0, @i1]) expect(subject.issues).to eq([@i0, @i1])
end end
...@@ -82,7 +82,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -82,7 +82,7 @@ describe Gitlab::ReferenceExtractor do
end end
it 'handles project issue references' do it 'handles project issue references' do
subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}") subject.analyze("this refers issue #{issue.to_reference(project)}")
extracted = subject.issues extracted = subject.issues
expect(extracted.size).to eq(1) expect(extracted.size).to eq(1)
expect(extracted).to eq([issue]) expect(extracted).to eq([issue])
......
require 'spec_helper' require 'spec_helper'
describe CommitRange do describe CommitRange do
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
let(:sha_from) { 'f3f85602' } let(:sha_from) { 'f3f85602' }
let(:sha_to) { 'e86e1013' } let(:sha_to) { 'e86e1013' }
...@@ -21,6 +27,23 @@ describe CommitRange do ...@@ -21,6 +27,23 @@ describe CommitRange do
end end
end end
describe '#to_reference' do
let(:project) { double('project', to_reference: 'namespace1/project') }
before do
range.project = project
end
it 'returns a String reference to the object' do
expect(range.to_reference).to eq range.to_s
end
it 'supports a cross-project reference' do
cross = double('project')
expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}"
end
end
describe '#reference_title' do describe '#reference_title' do
it 'returns the correct String for three-dot ranges' 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 #{sha_from} through #{sha_to}"
......
require 'spec_helper' require 'spec_helper'
describe Commit do describe Commit do
let(:project) { create :project } let(:project) { create(:project) }
let(:commit) { project.commit } let(:commit) { project.commit }
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Mentionable) }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(StaticModel) }
end
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
end
it 'supports a cross-project reference' do
cross = double('project')
expect(commit.to_reference(cross)).to eq "#{project.to_reference}@#{commit.id}"
end
end
describe '#title' do describe '#title' do
it "returns no_commit_message when safe_message is blank" do it "returns no_commit_message when safe_message is blank" do
......
require 'spec_helper'
describe ExternalIssue do
let(:project) { double('project', to_reference: 'namespace1/project1') }
let(:issue) { described_class.new('EXT-1234', project) }
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(issue.to_reference).to eq issue.id
end
end
describe '#title' do
it 'returns a title' do
expect(issue.title).to eq "External Issue #{issue}"
end
end
end
...@@ -18,16 +18,30 @@ require 'spec_helper' ...@@ -18,16 +18,30 @@ require 'spec_helper'
describe Group do describe Group do
let!(:group) { create(:group) } let!(:group) { create(:group) }
describe "Associations" do describe 'associations' do
it { is_expected.to have_many :projects } it { is_expected.to have_many :projects }
it { is_expected.to have_many :group_members } it { is_expected.to have_many :group_members }
end end
it { is_expected.to validate_presence_of :name } describe 'modules' do
it { is_expected.to validate_uniqueness_of(:name) } subject { described_class }
it { is_expected.to validate_presence_of :path }
it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.to include_module(Referable) }
it { is_expected.not_to validate_presence_of :owner } end
describe 'validations' do
it { is_expected.to validate_presence_of :name }
it { is_expected.to validate_uniqueness_of(:name) }
it { is_expected.to validate_presence_of :path }
it { is_expected.to validate_uniqueness_of(:path) }
it { is_expected.not_to validate_presence_of :owner }
end
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}"
end
end
describe :users do describe :users do
it { expect(group.users).to eq(group.owners) } it { expect(group.users).to eq(group.owners) }
......
...@@ -24,15 +24,30 @@ describe Issue do ...@@ -24,15 +24,30 @@ describe Issue do
it { is_expected.to belong_to(:milestone) } it { is_expected.to belong_to(:milestone) }
end end
describe "Mass assignment" do
end
describe 'modules' do describe 'modules' do
subject { described_class }
it { is_expected.to include_module(InternalId) }
it { is_expected.to include_module(Issuable) } it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
end end
subject { create(:issue) } subject { create(:issue) }
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "##{subject.iid}"
end
it 'supports a cross-project reference' do
cross = double('project')
expect(subject.to_reference(cross)).
to eq "#{subject.project.to_reference}##{subject.iid}"
end
end
describe '#is_being_reassigned?' do describe '#is_being_reassigned?' do
it 'returns true if the issue assignee has changed' do it 'returns true if the issue assignee has changed' do
subject.assignee = create(:user) subject.assignee = create(:user)
...@@ -45,11 +60,8 @@ describe Issue do ...@@ -45,11 +60,8 @@ describe Issue do
describe '#is_being_reassigned?' do describe '#is_being_reassigned?' do
it 'returns issues assigned to user' do it 'returns issues assigned to user' do
user = create :user user = create(:user)
create_list(:issue, 2, assignee: user)
2.times do
issue = create :issue, assignee: user
end
expect(Issue.open_for(user).count).to eq 2 expect(Issue.open_for(user).count).to eq 2
end end
......
...@@ -14,30 +14,63 @@ require 'spec_helper' ...@@ -14,30 +14,63 @@ require 'spec_helper'
describe Label do describe Label do
let(:label) { create(:label) } let(:label) { create(:label) }
it { expect(label).to be_valid }
it { is_expected.to belong_to(:project) } describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
describe 'Validation' do
it 'should validate color code' do it 'should validate color code' do
expect(build(:label, color: 'G-ITLAB')).not_to be_valid expect(label).not_to allow_value('G-ITLAB').for(:color)
expect(build(:label, color: 'AABBCC')).not_to be_valid expect(label).not_to allow_value('AABBCC').for(:color)
expect(build(:label, color: '#AABBCCEE')).not_to be_valid expect(label).not_to allow_value('#AABBCCEE').for(:color)
expect(build(:label, color: '#GGHHII')).not_to be_valid expect(label).not_to allow_value('GGHHII').for(:color)
expect(build(:label, color: '#')).not_to be_valid expect(label).not_to allow_value('#').for(:color)
expect(build(:label, color: '')).not_to be_valid expect(label).not_to allow_value('').for(:color)
expect(build(:label, color: '#AABBCC')).to be_valid expect(label).to allow_value('#AABBCC').for(:color)
expect(label).to allow_value('#abcdef').for(:color)
end end
it 'should validate title' do it 'should validate title' do
expect(build(:label, title: 'G,ITLAB')).not_to be_valid expect(label).not_to allow_value('G,ITLAB').for(:title)
expect(build(:label, title: 'G?ITLAB')).not_to be_valid expect(label).not_to allow_value('G?ITLAB').for(:title)
expect(build(:label, title: 'G&ITLAB')).not_to be_valid expect(label).not_to allow_value('G&ITLAB').for(:title)
expect(build(:label, title: '')).not_to be_valid expect(label).not_to allow_value('').for(:title)
expect(label).to allow_value('GITLAB').for(:title)
expect(label).to allow_value('gitlab').for(:title)
expect(label).to allow_value("customer's request").for(:title)
end
end
describe '#to_reference' do
context 'using id' do
it 'returns a String reference to the object' do
expect(label.to_reference).to eq "~#{label.id}"
expect(label.to_reference(double('project'))).to eq "~#{label.id}"
end
end
context 'using name' do
it 'returns a String reference to the object' do
expect(label.to_reference(:name)).to eq %(~"#{label.name}")
end
expect(build(:label, title: 'GITLAB')).to be_valid it 'uses id when name contains double quote' do
expect(build(:label, title: 'gitlab')).to be_valid label = create(:label, name: %q{"irony"})
expect(label.to_reference(:name)).to eq "~#{label.id}"
end
end end
end end
end end
...@@ -24,22 +24,45 @@ ...@@ -24,22 +24,45 @@
require 'spec_helper' require 'spec_helper'
describe MergeRequest do describe MergeRequest do
describe "Validation" do subject { create(:merge_request) }
it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) } describe 'associations' do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
end end
describe "Mass assignment" do describe 'modules' do
subject { described_class }
it { is_expected.to include_module(InternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
end end
describe "Respond to" do describe 'validation' do
it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) }
end
describe 'respond to' do
it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:unchecked?) }
it { is_expected.to respond_to(:can_be_merged?) } it { is_expected.to respond_to(:can_be_merged?) }
it { is_expected.to respond_to(:cannot_be_merged?) } it { is_expected.to respond_to(:cannot_be_merged?) }
end end
describe 'modules' do describe '#to_reference' do
it { is_expected.to include_module(Issuable) } it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
end
it 'supports a cross-project reference' do
cross = double('project')
expect(subject.to_reference(cross)).to eq "#{subject.source_project.to_reference}!#{subject.iid}"
end
end end
describe "#mr_and_commit_notes" do describe "#mr_and_commit_notes" do
...@@ -57,8 +80,6 @@ describe MergeRequest do ...@@ -57,8 +80,6 @@ describe MergeRequest do
end end
end end
subject { create(:merge_request) }
describe '#is_being_reassigned?' do describe '#is_being_reassigned?' do
it 'returns true if the merge_request assignee has changed' do it 'returns true if the merge_request assignee has changed' do
subject.assignee = create(:user) subject.assignee = create(:user)
...@@ -108,7 +129,7 @@ describe MergeRequest do ...@@ -108,7 +129,7 @@ describe MergeRequest do
it 'detects issues mentioned in the description' do it 'detects issues mentioned in the description' do
issue2 = create(:issue, project: subject.project) issue2 = create(:issue, project: subject.project)
subject.description = "Closes ##{issue2.iid}" subject.description = "Closes #{issue2.to_reference}"
subject.project.stub(default_branch: subject.target_branch) subject.project.stub(default_branch: subject.target_branch)
expect(subject.closes_issues).to include(issue2) expect(subject.closes_issues).to include(issue2)
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
require 'spec_helper' require 'spec_helper'
describe Project do describe Project do
describe 'Associations' do describe 'associations' do
it { is_expected.to belong_to(:group) } it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) } it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:creator).class_name('User') } it { is_expected.to belong_to(:creator).class_name('User') }
...@@ -54,10 +54,17 @@ describe Project do ...@@ -54,10 +54,17 @@ describe Project do
it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) }
end end
describe 'Mass assignment' do describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::ShellAdapter) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end end
describe 'Validation' do describe 'validation' do
let!(:project) { create(:project) } let!(:project) { create(:project) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
...@@ -91,6 +98,14 @@ describe Project do ...@@ -91,6 +98,14 @@ describe Project do
it { is_expected.to respond_to(:path_with_namespace) } it { is_expected.to respond_to(:path_with_namespace) }
end end
describe '#to_reference' do
let(:project) { create(:empty_project) }
it 'returns a String reference to the object' do
expect(project.to_reference).to eq project.path_with_namespace
end
end
it 'should return valid url to repo' do it 'should return valid url to repo' do
project = Project.new(path: 'somewhere') project = Project.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
......
...@@ -18,23 +18,47 @@ ...@@ -18,23 +18,47 @@
require 'spec_helper' require 'spec_helper'
describe Snippet do describe Snippet do
describe "Associations" do describe 'modules' do
it { is_expected.to belong_to(:author).class_name('User') } subject { described_class }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
it { is_expected.to include_module(Linguist::BlobHelper) }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end end
describe "Mass assignment" do describe 'associations' do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
end end
describe "Validation" do describe 'validation' do
it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:title) }
it { is_expected.to ensure_length_of(:title).is_within(0..255) } it { is_expected.to ensure_length_of(:title).is_within(0..255) }
it { is_expected.to validate_presence_of(:file_name) } it { is_expected.to validate_presence_of(:file_name) }
it { is_expected.to ensure_length_of(:title).is_within(0..255) } it { is_expected.to ensure_length_of(:file_name).is_within(0..255) }
it { is_expected.to validate_presence_of(:content) } it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) }
end
describe '#to_reference' do
let(:project) { create(:empty_project) }
let(:snippet) { create(:snippet, project: project) }
it 'returns a String reference to the object' do
expect(snippet.to_reference).to eq "$#{snippet.id}"
end
it 'supports a cross-project reference' do
cross = double('project')
expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}"
end
end end
end end
...@@ -63,7 +63,17 @@ require 'spec_helper' ...@@ -63,7 +63,17 @@ require 'spec_helper'
describe User do describe User do
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
describe "Associations" do describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
end
describe 'associations' do
it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) }
it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) }
...@@ -79,9 +89,6 @@ describe User do ...@@ -79,9 +89,6 @@ describe User do
it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) }
end end
describe "Mass assignment" do
end
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:username) } it { is_expected.to validate_presence_of(:username) }
it { is_expected.to validate_presence_of(:projects_limit) } it { is_expected.to validate_presence_of(:projects_limit) }
...@@ -175,6 +182,14 @@ describe User do ...@@ -175,6 +182,14 @@ describe User do
it { is_expected.to respond_to(:private_token) } it { is_expected.to respond_to(:private_token) }
end end
describe '#to_reference' do
let(:user) { create(:user) }
it 'returns a String reference to the object' do
expect(user.to_reference).to eq "@#{user.username}"
end
end
describe '#generate_password' do describe '#generate_password' do
it "should execute callback when force_random_password specified" do it "should execute callback when force_random_password specified" do
user = build(:user, force_random_password: true) user = build(:user, force_random_password: true)
......
...@@ -259,13 +259,13 @@ describe SystemNoteService do ...@@ -259,13 +259,13 @@ describe SystemNoteService do
let(:mentioner) { project2.repository.commit } let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do it 'references the mentioning commit' do
expect(subject.note).to eq "mentioned in commit #{project2.path_with_namespace}@#{mentioner.id}" expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
end end
end end
context 'from non-Commit' do context 'from non-Commit' do
it 'references the mentioning object' do it 'references the mentioning object' do
expect(subject.note).to eq "mentioned in issue #{project2.path_with_namespace}##{mentioner.iid}" expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
end end
end end
end end
...@@ -275,13 +275,13 @@ describe SystemNoteService do ...@@ -275,13 +275,13 @@ describe SystemNoteService do
let(:mentioner) { project.repository.commit } let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do it 'references the mentioning commit' do
expect(subject.note).to eq "mentioned in commit #{mentioner.id}" expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
end end
end end
context 'from non-Commit' do context 'from non-Commit' do
it 'references the mentioning object' do it 'references the mentioning object' do
expect(subject.note).to eq "mentioned in issue ##{mentioner.iid}" expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
end end
end end
end end
...@@ -291,7 +291,7 @@ describe SystemNoteService do ...@@ -291,7 +291,7 @@ describe SystemNoteService do
describe '.cross_reference?' do describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do it 'is truthy when text begins with expected text' do
expect(described_class.cross_reference?('mentioned in issue #1')).to be_truthy expect(described_class.cross_reference?('mentioned in something')).to be_truthy
end end
it 'is falsey when text does not begin with expected text' do it 'is falsey when text does not begin with expected text' do
......
...@@ -10,12 +10,12 @@ def common_mentionable_setup ...@@ -10,12 +10,12 @@ def common_mentionable_setup
let(:mentioned_issue) { create(:issue, project: project) } let(:mentioned_issue) { create(:issue, project: project) }
let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) } let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
let(:mentioned_commit) { project.repository.commit } let(:mentioned_commit) { project.commit }
let(:ext_proj) { create(:project, :public) } let(:ext_proj) { create(:project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) } let(:ext_issue) { create(:issue, project: ext_proj) }
let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) } let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
let(:ext_commit) { ext_proj.repository.commit } let(:ext_commit) { ext_proj.commit }
# Override to add known commits to the repository stub. # Override to add known commits to the repository stub.
let(:extra_commits) { [] } let(:extra_commits) { [] }
...@@ -23,21 +23,19 @@ def common_mentionable_setup ...@@ -23,21 +23,19 @@ def common_mentionable_setup
# A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference # A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference
# to this string and place it in their +mentionable_text+. # to this string and place it in their +mentionable_text+.
let(:ref_string) do let(:ref_string) do
cross = ext_proj.path_with_namespace
<<-MSG.strip_heredoc <<-MSG.strip_heredoc
These references are new: These references are new:
Issue: ##{mentioned_issue.iid} Issue: #{mentioned_issue.to_reference}
Merge: !#{mentioned_mr.iid} Merge: #{mentioned_mr.to_reference}
Commit: #{mentioned_commit.id} Commit: #{mentioned_commit.to_reference}
This reference is a repeat and should only be mentioned once: This reference is a repeat and should only be mentioned once:
Repeat: ##{mentioned_issue.iid} Repeat: #{mentioned_issue.to_reference}
These references are cross-referenced: These references are cross-referenced:
Issue: #{cross}##{ext_issue.iid} Issue: #{ext_issue.to_reference(project)}
Merge: #{cross}!#{ext_mr.iid} Merge: #{ext_mr.to_reference(project)}
Commit: #{cross}@#{ext_commit.short_id} Commit: #{ext_commit.to_reference(project)}
This is a self-reference and should not be mentioned at all: This is a self-reference and should not be mentioned at all:
Self: #{backref_text} Self: #{backref_text}
...@@ -109,19 +107,17 @@ shared_examples 'an editable mentionable' do ...@@ -109,19 +107,17 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save subject.save
cross = ext_proj.path_with_namespace
new_text = <<-MSG new_text = <<-MSG
These references already existed: These references already existed:
Issue: ##{mentioned_issue.iid} Issue: #{mentioned_issue.to_reference}
Commit: #{mentioned_commit.id} Commit: #{mentioned_commit.to_reference}
This cross-project reference already existed: This cross-project reference already existed:
Issue: #{cross}##{ext_issue.iid} Issue: #{ext_issue.to_reference(project)}
These two references are introduced in an edit: These two references are introduced in an edit:
Issue: ##{new_issues[0].iid} Issue: #{new_issues[0].to_reference}
Cross: #{cross}##{new_issues[1].iid} Cross: #{new_issues[1].to_reference(project)}
MSG MSG
# These three objects were already referenced, and should not receive new # These three objects were already referenced, and should not receive new
......
...@@ -10,6 +10,26 @@ module ReferenceFilterSpecHelper ...@@ -10,6 +10,26 @@ module ReferenceFilterSpecHelper
Rails.application.routes.url_helpers Rails.application.routes.url_helpers
end end
# Modify a String reference to make it invalid
#
# Commit SHAs get reversed, IDs get incremented by 1, all other Strings get
# their word characters reversed.
#
# reference - String reference to modify
#
# Returns a String
def invalidate_reference(reference)
if reference =~ /\A(.+)?.\d+\z/
# Integer-based reference with optional project prefix
reference.gsub(/\d+\z/) { |i| i.to_i + 1 }
elsif reference =~ /\A(.+@)?(\h{6,40}\z)/
# SHA-based reference with optional prefix
reference.gsub(/\h{6,40}\z/) { |v| v.reverse }
else
reference.gsub(/\w+\z/) { |v| v.reverse }
end
end
# Perform `call` on the described class # Perform `call` on the described class
# #
# Automatically passes the current `project` value to the context if none is # Automatically passes the current `project` value to the context if none is
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment