Merge branch 'rs-reference-filters' into 'master'
Convert GFM reference handling to html-pipeline filters - `Gitlab::Markdown` is now much cleaner - Better separation of concerns - Cleaner, less brittle, more maintainable specs for each reference type. - Label references actually work! See merge request !1753
This commit is contained in:
commit
2cd501f7e4
|
@ -74,6 +74,7 @@ module GitlabMarkdownHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): This should be its own filter
|
||||||
def create_relative_links(text)
|
def create_relative_links(text)
|
||||||
paths = extract_paths(text)
|
paths = extract_paths(text)
|
||||||
|
|
||||||
|
|
|
@ -108,4 +108,7 @@ module IssuesHelper
|
||||||
xml.summary issue.title
|
xml.summary issue.title
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Required for Gitlab::Markdown::IssueReferenceFilter
|
||||||
|
module_function :url_for_issue, :title_for_issue
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
module LabelsHelper
|
module LabelsHelper
|
||||||
|
include ActionView::Helpers::TagHelper
|
||||||
|
|
||||||
def project_label_names
|
def project_label_names
|
||||||
@project.labels.pluck(:title)
|
@project.labels.pluck(:title)
|
||||||
end
|
end
|
||||||
|
@ -7,9 +9,13 @@ module LabelsHelper
|
||||||
label_color = label.color || Label::DEFAULT_COLOR
|
label_color = label.color || Label::DEFAULT_COLOR
|
||||||
text_color = text_color_for_bg(label_color)
|
text_color = text_color_for_bg(label_color)
|
||||||
|
|
||||||
content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do
|
# Intentionally not using content_tag here so that this method can be called
|
||||||
label.name
|
# by LabelReferenceFilter
|
||||||
end
|
span = %(<span class="label color-label") +
|
||||||
|
%( style="background-color: #{label_color}; color: #{text_color}">) +
|
||||||
|
escape_once(label.name) + '</span>'
|
||||||
|
|
||||||
|
span.html_safe
|
||||||
end
|
end
|
||||||
|
|
||||||
def suggested_colors
|
def suggested_colors
|
||||||
|
@ -42,13 +48,16 @@ module LabelsHelper
|
||||||
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
|
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
|
||||||
|
|
||||||
if (r + g + b) > 500
|
if (r + g + b) > 500
|
||||||
"#333"
|
'#333333'
|
||||||
else
|
else
|
||||||
"#FFF"
|
'#FFFFFF'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_labels_options(project)
|
def project_labels_options(project)
|
||||||
options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
|
options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Required for Gitlab::Markdown::LabelReferenceFilter
|
||||||
|
module_function :render_colored_label, :text_color_for_bg, :escape_once
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Label < ActiveRecord::Base
|
||||||
# Don't allow '?', '&', and ',' for label titles
|
# Don't allow '?', '&', and ',' for label titles
|
||||||
validates :title,
|
validates :title,
|
||||||
presence: true,
|
presence: true,
|
||||||
format: { with: /\A[^&\?,&]+\z/ },
|
format: { with: /\A[^&\?,]+\z/ },
|
||||||
uniqueness: { scope: :project_id }
|
uniqueness: { scope: :project_id }
|
||||||
|
|
||||||
default_scope { order(title: :asc) }
|
default_scope { order(title: :asc) }
|
||||||
|
|
|
@ -163,7 +163,7 @@ Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported
|
||||||
|
|
||||||
## Special GitLab References
|
## Special GitLab References
|
||||||
|
|
||||||
GFM recognized special references.
|
GFM recognizes special references.
|
||||||
|
|
||||||
You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
|
You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
|
||||||
|
|
||||||
|
@ -171,19 +171,30 @@ GFM will turn that reference into a link so you can navigate between them easily
|
||||||
|
|
||||||
GFM will recognize the following:
|
GFM will recognize the following:
|
||||||
|
|
||||||
- @foo : for specific team members or groups
|
| input | references |
|
||||||
- @all : for the whole team
|
|-----------------------:|:---------------------------|
|
||||||
- #123 : for issues
|
| `@user_name` | specific user |
|
||||||
- !123 : for merge requests
|
| `@group_name` | specific group |
|
||||||
- $123 : for snippets
|
| `@all` | entire team |
|
||||||
- 1234567 : for commits
|
| `#123` | issue |
|
||||||
- \[file\](path/to/file) : for file references
|
| `!123` | merge request |
|
||||||
|
| `$123` | snippet |
|
||||||
|
| `~123` | label by ID |
|
||||||
|
| `~bug` | one-word label by name |
|
||||||
|
| `~"feature request"` | multi-word label by name |
|
||||||
|
| `9ba12248` | specific commit |
|
||||||
|
| `9ba12248...b19a04f5` | commit range comparison |
|
||||||
|
| `[README](doc/README)` | repository file references |
|
||||||
|
|
||||||
GFM also recognizes references to commits, issues, and merge requests in other projects:
|
GFM also recognizes certain cross-project references:
|
||||||
|
|
||||||
- namespace/project#123 : for issues
|
| input | references |
|
||||||
- namespace/project!123 : for merge requests
|
|----------------------------------------:|:------------------------|
|
||||||
- namespace/project@1234567 : for commits
|
| `namespace/project#123` | issue |
|
||||||
|
| `namespace/project!123` | merge request |
|
||||||
|
| `namespace/project$123` | snippet |
|
||||||
|
| `namespace/project@9ba12248` | specific commit |
|
||||||
|
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
|
||||||
|
|
||||||
## Task Lists
|
## Task Lists
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,11 @@ module Gitlab
|
||||||
# Supported reference formats are:
|
# Supported reference formats are:
|
||||||
# * @foo for team members
|
# * @foo for team members
|
||||||
# * #123 for issues
|
# * #123 for issues
|
||||||
# * #JIRA-123 for Jira issues
|
# * JIRA-123 for Jira issues
|
||||||
# * !123 for merge requests
|
# * !123 for merge requests
|
||||||
# * $123 for snippets
|
# * $123 for snippets
|
||||||
# * 123456 for commits
|
# * 1c002d for specific commit
|
||||||
# * 123456...7890123 for commit ranges (comparisons)
|
# * 1c002d...35cfb2 for commit ranges (comparisons)
|
||||||
#
|
#
|
||||||
# It also parses Emoji codes to insert images. See
|
# It also parses Emoji codes to insert images. See
|
||||||
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
|
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
|
||||||
|
@ -30,10 +30,6 @@ module Gitlab
|
||||||
# >> gfm(":trollface:")
|
# >> gfm(":trollface:")
|
||||||
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
|
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
|
||||||
module Markdown
|
module Markdown
|
||||||
include IssuesHelper
|
|
||||||
|
|
||||||
attr_reader :options, :html_options
|
|
||||||
|
|
||||||
# Public: Parse the provided text with GitLab-Flavored Markdown
|
# Public: Parse the provided text with GitLab-Flavored Markdown
|
||||||
#
|
#
|
||||||
# text - the source text
|
# text - the source text
|
||||||
|
@ -65,42 +61,14 @@ module Gitlab
|
||||||
reference_only_path: true
|
reference_only_path: true
|
||||||
)
|
)
|
||||||
|
|
||||||
@options = options
|
|
||||||
@html_options = html_options
|
|
||||||
|
|
||||||
# TODO: add popups with additional information
|
|
||||||
|
|
||||||
# Used markdown pipelines in GitLab:
|
|
||||||
# GitlabEmojiFilter - performs emoji replacement.
|
|
||||||
# SanitizationFilter - remove unsafe HTML tags and attributes
|
|
||||||
#
|
|
||||||
# see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
|
|
||||||
filters = [
|
|
||||||
HTML::Pipeline::Gitlab::GitlabEmojiFilter,
|
|
||||||
HTML::Pipeline::SanitizationFilter
|
|
||||||
]
|
|
||||||
|
|
||||||
whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
|
|
||||||
whitelist[:attributes][:all].push('class', 'id')
|
|
||||||
whitelist[:elements].push('span')
|
|
||||||
|
|
||||||
# Remove the rel attribute that the sanitize gem adds, and remove the
|
|
||||||
# href attribute if it contains inline javascript
|
|
||||||
fix_anchors = lambda do |env|
|
|
||||||
name, node = env[:node_name], env[:node]
|
|
||||||
if name == 'a'
|
|
||||||
node.remove_attribute('rel')
|
|
||||||
if node['href'] && node['href'].match('javascript:')
|
|
||||||
node.remove_attribute('href')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
whitelist[:transformers].push(fix_anchors)
|
|
||||||
|
|
||||||
markdown_context = {
|
markdown_context = {
|
||||||
asset_root: Gitlab.config.gitlab.url,
|
asset_root: Gitlab.config.gitlab.url,
|
||||||
asset_host: Gitlab::Application.config.asset_host,
|
asset_host: Gitlab::Application.config.asset_host,
|
||||||
whitelist: whitelist
|
whitelist: sanitization_whitelist,
|
||||||
|
reference_class: html_options[:class],
|
||||||
|
only_path: options[:reference_only_path],
|
||||||
|
current_user: current_user,
|
||||||
|
project: project
|
||||||
}
|
}
|
||||||
|
|
||||||
markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
|
markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
|
||||||
|
@ -114,21 +82,6 @@ module Gitlab
|
||||||
|
|
||||||
text = result[:output].to_html(save_with: save_options)
|
text = result[:output].to_html(save_with: save_options)
|
||||||
|
|
||||||
# Extract pre blocks so they are not altered
|
|
||||||
# from http://github.github.com/github-flavored-markdown/
|
|
||||||
text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) }
|
|
||||||
# Extract links with probably parsable hrefs
|
|
||||||
text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) }
|
|
||||||
# Extract images with probably parsable src
|
|
||||||
text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) }
|
|
||||||
|
|
||||||
text = parse(text, project)
|
|
||||||
|
|
||||||
# Insert pre block extractions
|
|
||||||
text.gsub!(/\{gfm-extraction-(\h{32})\}/) do
|
|
||||||
insert_piece($1)
|
|
||||||
end
|
|
||||||
|
|
||||||
if options[:parse_tasks]
|
if options[:parse_tasks]
|
||||||
text = parse_tasks(text)
|
text = parse_tasks(text)
|
||||||
end
|
end
|
||||||
|
@ -138,242 +91,53 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def extract_piece(text)
|
# Custom filters for html-pipeline:
|
||||||
@extractions ||= {}
|
|
||||||
|
|
||||||
md5 = Digest::MD5.hexdigest(text)
|
|
||||||
@extractions[md5] = text
|
|
||||||
"{gfm-extraction-#{md5}}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def insert_piece(id)
|
|
||||||
@extractions[id]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Private: Parses text for references
|
|
||||||
#
|
#
|
||||||
# text - Text to parse
|
# SanitizationFilter should come first so that all generated reference HTML
|
||||||
|
# goes through untouched.
|
||||||
#
|
#
|
||||||
# Returns parsed text
|
# See https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
|
||||||
def parse(text, project = @project)
|
def filters
|
||||||
parse_references(text, project) if project
|
[
|
||||||
|
HTML::Pipeline::SanitizationFilter,
|
||||||
|
|
||||||
text
|
Gitlab::Markdown::UserReferenceFilter,
|
||||||
|
Gitlab::Markdown::IssueReferenceFilter,
|
||||||
|
Gitlab::Markdown::ExternalIssueReferenceFilter,
|
||||||
|
Gitlab::Markdown::MergeRequestReferenceFilter,
|
||||||
|
Gitlab::Markdown::SnippetReferenceFilter,
|
||||||
|
Gitlab::Markdown::CommitRangeReferenceFilter,
|
||||||
|
Gitlab::Markdown::CommitReferenceFilter,
|
||||||
|
Gitlab::Markdown::LabelReferenceFilter,
|
||||||
|
|
||||||
|
HTML::Pipeline::Gitlab::GitlabEmojiFilter
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
|
# Customize the SanitizationFilter whitelist
|
||||||
PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
|
|
||||||
|
|
||||||
REFERENCE_PATTERN = %r{
|
|
||||||
(?<prefix>\W)? # Prefix
|
|
||||||
( # Reference
|
|
||||||
@(?<user>#{NAME_STR}) # User name
|
|
||||||
|~(?<label>\d+) # Label ID
|
|
||||||
|(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
|
|
||||||
|#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
|
|
||||||
|#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
|
|
||||||
|\$(?<snippet>\d+) # Snippet ID
|
|
||||||
|(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
|
|
||||||
|(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
|
|
||||||
|(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
|
|
||||||
)
|
|
||||||
(?<suffix>\W)? # Suffix
|
|
||||||
}x.freeze
|
|
||||||
|
|
||||||
TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze
|
|
||||||
|
|
||||||
def parse_references(text, project = @project)
|
|
||||||
# parse reference links
|
|
||||||
text.gsub!(REFERENCE_PATTERN) do |match|
|
|
||||||
type = TYPES.select{|t| !$~[t].nil?}.first
|
|
||||||
|
|
||||||
actual_project = project
|
|
||||||
project_prefix = nil
|
|
||||||
project_path = $LAST_MATCH_INFO[:project]
|
|
||||||
if project_path
|
|
||||||
actual_project = ::Project.find_with_namespace(project_path)
|
|
||||||
actual_project = nil unless can?(current_user, :read_project, actual_project)
|
|
||||||
project_prefix = project_path
|
|
||||||
end
|
|
||||||
|
|
||||||
parse_result($LAST_MATCH_INFO, type,
|
|
||||||
actual_project, project_prefix) || match
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Called from #parse_references. Attempts to build a gitlab reference
|
|
||||||
# link. Returns nil if +type+ is nil, if the match string is an HTML
|
|
||||||
# entity, if the reference is invalid, or if the matched text includes an
|
|
||||||
# invalid project path.
|
|
||||||
def parse_result(match_info, type, project, project_prefix)
|
|
||||||
prefix = match_info[:prefix]
|
|
||||||
suffix = match_info[:suffix]
|
|
||||||
|
|
||||||
return nil if html_entity?(prefix, suffix) || type.nil?
|
|
||||||
return nil if project.nil? && !project_prefix.nil?
|
|
||||||
|
|
||||||
identifier = match_info[type]
|
|
||||||
ref_link = reference_link(type, identifier, project, project_prefix)
|
|
||||||
|
|
||||||
if ref_link
|
|
||||||
"#{prefix}#{ref_link}#{suffix}"
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return true if the +prefix+ and +suffix+ indicate that the matched string
|
|
||||||
# is an HTML entity like &
|
|
||||||
def html_entity?(prefix, suffix)
|
|
||||||
prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Private: Dispatches to a dedicated processing method based on reference
|
|
||||||
#
|
#
|
||||||
# reference - Object reference ("@1234", "!567", etc.)
|
# - Allow `class` and `id` attributes on all elements
|
||||||
# identifier - Object identifier (Issue ID, SHA hash, etc.)
|
# - Allow `span` elements
|
||||||
#
|
# - Remove `rel` attributes from `a` elements
|
||||||
# Returns string rendered by the processing method
|
# - Remove `a` nodes with `javascript:` in the `href` attribute
|
||||||
def reference_link(type, identifier, project = @project, prefix_text = nil)
|
def sanitization_whitelist
|
||||||
send("reference_#{type}", identifier, project, prefix_text)
|
whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
|
||||||
end
|
whitelist[:attributes][:all].push('class', 'id')
|
||||||
|
whitelist[:elements].push('span')
|
||||||
|
|
||||||
def reference_user(identifier, project = @project, _ = nil)
|
fix_anchors = lambda do |env|
|
||||||
link_options = html_options.merge(
|
name, node = env[:node_name], env[:node]
|
||||||
class: "gfm gfm-project_member #{html_options[:class]}"
|
if name == 'a'
|
||||||
)
|
node.remove_attribute('rel')
|
||||||
|
if node['href'] && node['href'].match('javascript:')
|
||||||
if identifier == "all"
|
node.remove_attribute('href')
|
||||||
link_to(
|
|
||||||
"@all",
|
|
||||||
namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]),
|
|
||||||
link_options
|
|
||||||
)
|
|
||||||
elsif namespace = Namespace.find_by(path: identifier)
|
|
||||||
url =
|
|
||||||
if namespace.is_a?(Group)
|
|
||||||
return nil unless can?(current_user, :read_group, namespace)
|
|
||||||
group_url(identifier, only_path: options[:reference_only_path])
|
|
||||||
else
|
|
||||||
user_url(identifier, only_path: options[:reference_only_path])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
link_to("@#{identifier}", url, link_options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_label(identifier, project = @project, _ = nil)
|
|
||||||
if label = project.labels.find_by(id: identifier)
|
|
||||||
link_options = html_options.merge(
|
|
||||||
class: "gfm gfm-label #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
link_to(
|
|
||||||
render_colored_label(label),
|
|
||||||
namespace_project_issues_path(project.namespace, project, label_name: label.name),
|
|
||||||
link_options
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_issue(identifier, project = @project, prefix_text = nil)
|
|
||||||
if project.default_issues_tracker?
|
|
||||||
if project.issue_exists? identifier
|
|
||||||
url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
|
|
||||||
title = title_for_issue(identifier, project)
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: "Issue: #{title}",
|
|
||||||
class: "gfm gfm-issue #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
link_to("#{prefix_text}##{identifier}", url, link_options)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if project.external_issue_tracker.present?
|
|
||||||
reference_external_issue(identifier, project,
|
|
||||||
prefix_text)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def reference_merge_request(identifier, project = @project, prefix_text = nil)
|
whitelist[:transformers].push(fix_anchors)
|
||||||
if merge_request = project.merge_requests.find_by(iid: identifier)
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: "Merge Request: #{merge_request.title}",
|
|
||||||
class: "gfm gfm-merge_request #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
url = namespace_project_merge_request_url(project.namespace, project,
|
|
||||||
merge_request,
|
|
||||||
only_path: options[:reference_only_path])
|
|
||||||
link_to("#{prefix_text}!#{identifier}", url, link_options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_snippet(identifier, project = @project, _ = nil)
|
whitelist
|
||||||
if snippet = project.snippets.find_by(id: identifier)
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: "Snippet: #{snippet.title}",
|
|
||||||
class: "gfm gfm-snippet #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
link_to(
|
|
||||||
"$#{identifier}",
|
|
||||||
namespace_project_snippet_url(project.namespace, project, snippet,
|
|
||||||
only_path: options[:reference_only_path]),
|
|
||||||
link_options
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_commit(identifier, project = @project, prefix_text = nil)
|
|
||||||
if project.valid_repo? && commit = project.repository.commit(identifier)
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: commit.link_title,
|
|
||||||
class: "gfm gfm-commit #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
prefix_text = "#{prefix_text}@" if prefix_text
|
|
||||||
link_to(
|
|
||||||
"#{prefix_text}#{identifier}",
|
|
||||||
namespace_project_commit_url( project.namespace, project, commit,
|
|
||||||
only_path: options[:reference_only_path]),
|
|
||||||
link_options
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_commit_range(identifier, project = @project, prefix_text = nil)
|
|
||||||
from_id, to_id = identifier.split(/\.{2,3}/, 2)
|
|
||||||
|
|
||||||
inclusive = identifier !~ /\.{3}/
|
|
||||||
from_id << "^" if inclusive
|
|
||||||
|
|
||||||
if project.valid_repo? &&
|
|
||||||
from = project.repository.commit(from_id) &&
|
|
||||||
to = project.repository.commit(to_id)
|
|
||||||
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: "Commits #{from_id} through #{to_id}",
|
|
||||||
class: "gfm gfm-commit_range #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
prefix_text = "#{prefix_text}@" if prefix_text
|
|
||||||
|
|
||||||
link_to(
|
|
||||||
"#{prefix_text}#{identifier}",
|
|
||||||
namespace_project_compare_url(project.namespace, project,
|
|
||||||
from: from_id, to: to_id,
|
|
||||||
only_path: options[:reference_only_path]),
|
|
||||||
link_options
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reference_external_issue(identifier, project = @project, prefix_text = nil)
|
|
||||||
url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
|
|
||||||
title = project.external_issue_tracker.title
|
|
||||||
|
|
||||||
link_options = html_options.merge(
|
|
||||||
title: "Issue in #{title}",
|
|
||||||
class: "gfm gfm-issue #{html_options[:class]}"
|
|
||||||
)
|
|
||||||
link_to("#{prefix_text}##{identifier}", url, link_options)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Turn list items that start with "[ ]" into HTML checkbox inputs.
|
# Turn list items that start with "[ ]" into HTML checkbox inputs.
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces commit range references with links.
|
||||||
|
#
|
||||||
|
# This filter supports cross-project references.
|
||||||
|
class CommitRangeReferenceFilter < ReferenceFilter
|
||||||
|
include CrossProjectReference
|
||||||
|
|
||||||
|
# Public: Find commit range references in text
|
||||||
|
#
|
||||||
|
# CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
|
||||||
|
# "<a href=...>#{commit_range}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, the String commit range, and an optional String
|
||||||
|
# of the external project reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(COMMIT_RANGE_PATTERN) do |match|
|
||||||
|
yield match, $~[:commit_range], $~[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(*args)
|
||||||
|
super
|
||||||
|
|
||||||
|
@commit_map = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract commit range references from text
|
||||||
|
#
|
||||||
|
# The beginning and ending SHA1 sums can be between 6 and 40 hex
|
||||||
|
# characters, and the range selection can be double- or triple-dot.
|
||||||
|
#
|
||||||
|
# This pattern supports cross-project references.
|
||||||
|
COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>\h{6,40}\.{2,3}\h{6,40})/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content|
|
||||||
|
commit_range_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace commit range references in text with links to compare the commit
|
||||||
|
# ranges.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with commit range references replaced with links. All
|
||||||
|
# links have `gfm` and `gfm-commit_range` class names attached for
|
||||||
|
# styling.
|
||||||
|
def commit_range_link_filter(text)
|
||||||
|
self.class.references_in(text) do |match, commit_range, project_ref|
|
||||||
|
project = self.project_from_ref(project_ref)
|
||||||
|
|
||||||
|
from_id, to_id = split_commit_range(commit_range)
|
||||||
|
|
||||||
|
if valid_range?(project, from_id, to_id)
|
||||||
|
url = url_for_commit_range(project, from_id, to_id)
|
||||||
|
|
||||||
|
title = "Commits #{from_id} through #{to_id}"
|
||||||
|
klass = reference_class(:commit_range)
|
||||||
|
|
||||||
|
project_ref += '@' if project_ref
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{project_ref}#{commit_range}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def split_commit_range(range)
|
||||||
|
from_id, to_id = range.split(/\.{2,3}/, 2)
|
||||||
|
from_id << "^" if range !~ /\.{3}/
|
||||||
|
|
||||||
|
[from_id, to_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit(id)
|
||||||
|
unless @commit_map[id]
|
||||||
|
@commit_map[id] = project.repository.commit(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@commit_map[id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_range?(project, from_id, to_id)
|
||||||
|
project && project.valid_repo? && commit(from_id) && commit(to_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_commit_range(project, from_id, to_id)
|
||||||
|
h = Rails.application.routes.url_helpers
|
||||||
|
h.namespace_project_compare_url(project.namespace, project,
|
||||||
|
from: from_id, to: to_id,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,80 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces commit references with links.
|
||||||
|
#
|
||||||
|
# This filter supports cross-project references.
|
||||||
|
class CommitReferenceFilter < ReferenceFilter
|
||||||
|
include CrossProjectReference
|
||||||
|
|
||||||
|
# Public: Find commit references in text
|
||||||
|
#
|
||||||
|
# CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
|
||||||
|
# "<a href=...>#{commit}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, the String commit identifier, and an optional
|
||||||
|
# String of the external project reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(COMMIT_PATTERN) do |match|
|
||||||
|
yield match, $~[:commit], $~[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract commit references from text
|
||||||
|
#
|
||||||
|
# The SHA1 sum can be between 6 and 40 hex characters.
|
||||||
|
#
|
||||||
|
# This pattern supports cross-project references.
|
||||||
|
COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(COMMIT_PATTERN) do |content|
|
||||||
|
commit_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace commit references in text with links to the commit specified.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with commit references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-commit` class names attached for styling.
|
||||||
|
def commit_link_filter(text)
|
||||||
|
self.class.references_in(text) do |match, commit_ref, project_ref|
|
||||||
|
project = self.project_from_ref(project_ref)
|
||||||
|
|
||||||
|
if commit = commit_from_ref(project, commit_ref)
|
||||||
|
url = url_for_commit(project, commit)
|
||||||
|
|
||||||
|
title = escape_once(commit.link_title)
|
||||||
|
klass = reference_class(:commit)
|
||||||
|
|
||||||
|
project_ref += '@' if project_ref
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{project_ref}#{commit_ref}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit_from_ref(project, commit_ref)
|
||||||
|
if project && project.valid_repo?
|
||||||
|
project.repository.commit(commit_ref)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_commit(project, commit)
|
||||||
|
h = Rails.application.routes.url_helpers
|
||||||
|
h.namespace_project_commit_url(project.namespace, project, commit,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# Common methods for ReferenceFilters that support an optional cross-project
|
||||||
|
# reference.
|
||||||
|
module CrossProjectReference
|
||||||
|
NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR
|
||||||
|
PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
|
||||||
|
|
||||||
|
# Given a cross-project reference string, get the Project record
|
||||||
|
#
|
||||||
|
# Defaults to value of `context[:project]` if:
|
||||||
|
# * No reference is given OR
|
||||||
|
# * Reference given doesn't exist
|
||||||
|
#
|
||||||
|
# ref - String reference.
|
||||||
|
#
|
||||||
|
# Returns a Project, or nil if the reference can't be accessed
|
||||||
|
def project_from_ref(ref)
|
||||||
|
return context[:project] unless ref
|
||||||
|
|
||||||
|
other = Project.find_with_namespace(ref)
|
||||||
|
return nil unless other && user_can_reference_project?(other)
|
||||||
|
|
||||||
|
other
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_can_reference_project?(project, user = context[:current_user])
|
||||||
|
Ability.abilities.allowed?(user, :read_project, project)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,63 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces external issue tracker references with links.
|
||||||
|
# References are ignored if the project doesn't use an external issue
|
||||||
|
# tracker.
|
||||||
|
class ExternalIssueReferenceFilter < ReferenceFilter
|
||||||
|
# Public: Find `JIRA-123` issue references in text
|
||||||
|
#
|
||||||
|
# ExternalIssueReferenceFilter.references_in(text) do |match, issue|
|
||||||
|
# "<a href=...>##{issue}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match and the String issue reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(ISSUE_PATTERN) do |match|
|
||||||
|
yield match, $~[:issue]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract `JIRA-123` issue references from text
|
||||||
|
ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
|
||||||
|
|
||||||
|
def call
|
||||||
|
# Early return if the project isn't using an external tracker
|
||||||
|
return doc if project.nil? || project.default_issues_tracker?
|
||||||
|
|
||||||
|
replace_text_nodes_matching(ISSUE_PATTERN) do |content|
|
||||||
|
issue_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace `JIRA-123` issue references in text with links to the referenced
|
||||||
|
# issue's details page.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with `JIRA-123` references replaced with links. All
|
||||||
|
# links have `gfm` and `gfm-issue` class names attached for styling.
|
||||||
|
def issue_link_filter(text)
|
||||||
|
project = context[:project]
|
||||||
|
|
||||||
|
self.class.references_in(text) do |match, issue|
|
||||||
|
url = url_for_issue(issue, project, only_path: context[:only_path])
|
||||||
|
|
||||||
|
title = escape_once("Issue in #{project.external_issue_tracker.title}")
|
||||||
|
klass = reference_class(:issue)
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{issue}</a>)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_issue(*args)
|
||||||
|
IssuesHelper.url_for_issue(*args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,74 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces issue references with links. References to
|
||||||
|
# issues that do not exist are ignored.
|
||||||
|
#
|
||||||
|
# This filter supports cross-project references.
|
||||||
|
class IssueReferenceFilter < ReferenceFilter
|
||||||
|
include CrossProjectReference
|
||||||
|
|
||||||
|
# Public: Find `#123` issue references in text
|
||||||
|
#
|
||||||
|
# IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
|
||||||
|
# "<a href=...>##{issue}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, the Integer issue ID, and an optional String of
|
||||||
|
# the external project reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(ISSUE_PATTERN) do |match|
|
||||||
|
yield match, $~[:issue].to_i, $~[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract `#123` issue references from text
|
||||||
|
#
|
||||||
|
# This pattern supports cross-project references.
|
||||||
|
ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(ISSUE_PATTERN) do |content|
|
||||||
|
issue_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace `#123` issue references in text with links to the referenced
|
||||||
|
# issue's details page.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with `#123` references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-issue` class names attached for styling.
|
||||||
|
def issue_link_filter(text)
|
||||||
|
self.class.references_in(text) do |match, issue, project_ref|
|
||||||
|
project = self.project_from_ref(project_ref)
|
||||||
|
|
||||||
|
if project && project.issue_exists?(issue)
|
||||||
|
url = url_for_issue(issue, project, only_path: context[:only_path])
|
||||||
|
|
||||||
|
title = escape_once("Issue: #{title_for_issue(issue, project)}")
|
||||||
|
klass = reference_class(:issue)
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{project_ref}##{issue}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_issue(*args)
|
||||||
|
IssuesHelper.url_for_issue(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def title_for_issue(*args)
|
||||||
|
IssuesHelper.title_for_issue(*args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,94 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces label references with links.
|
||||||
|
class LabelReferenceFilter < ReferenceFilter
|
||||||
|
# Public: Find label references in text
|
||||||
|
#
|
||||||
|
# LabelReferenceFilter.references_in(text) do |match, id, name|
|
||||||
|
# "<a href=...>#{Label.find(id)}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, an optional Integer label ID, and an optional
|
||||||
|
# String label name.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(LABEL_PATTERN) do |match|
|
||||||
|
yield match, $~[:label_id].to_i, $~[:label_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract label references from text
|
||||||
|
#
|
||||||
|
# TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
|
||||||
|
LABEL_PATTERN = %r{
|
||||||
|
~(
|
||||||
|
(?<label_id>\d+) | # Integer-based label ID, or
|
||||||
|
(?<label_name>
|
||||||
|
[A-Za-z0-9_-]+ | # String-based single-word label title
|
||||||
|
['"][^&\?,]+['"] # String-based multi-word label surrounded in quotes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}x
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(LABEL_PATTERN) do |content|
|
||||||
|
label_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace label references in text with links to the label specified.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with label references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-label` class names attached for styling.
|
||||||
|
def label_link_filter(text)
|
||||||
|
project = context[:project]
|
||||||
|
|
||||||
|
self.class.references_in(text) do |match, id, name|
|
||||||
|
params = label_params(id, name)
|
||||||
|
|
||||||
|
if label = project.labels.find_by(params)
|
||||||
|
url = url_for_label(project, label)
|
||||||
|
|
||||||
|
klass = reference_class(:label)
|
||||||
|
|
||||||
|
%(<a href="#{url}" class="#{klass}">#{render_colored_label(label)}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_label(project, label)
|
||||||
|
h = Rails.application.routes.url_helpers
|
||||||
|
h.namespace_project_issues_path(project.namespace, project,
|
||||||
|
label_name: label.name,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_colored_label(label)
|
||||||
|
LabelsHelper.render_colored_label(label)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parameters to pass to `Label.find_by` based on the given arguments
|
||||||
|
#
|
||||||
|
# id - Integer ID to pass. If present, returns {id: id}
|
||||||
|
# name - String name to pass. If `id` is absent, finds by name without
|
||||||
|
# surrounding quotes.
|
||||||
|
#
|
||||||
|
# Returns a Hash.
|
||||||
|
def label_params(id, name)
|
||||||
|
if id > 0
|
||||||
|
{ id: id }
|
||||||
|
else
|
||||||
|
# TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding.
|
||||||
|
{ name: name.tr('\'"', '') }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,73 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces merge request references with links. References
|
||||||
|
# to merge requests that do not exist are ignored.
|
||||||
|
#
|
||||||
|
# This filter supports cross-project references.
|
||||||
|
class MergeRequestReferenceFilter < ReferenceFilter
|
||||||
|
include CrossProjectReference
|
||||||
|
|
||||||
|
# Public: Find `!123` merge request references in text
|
||||||
|
#
|
||||||
|
# MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
|
||||||
|
# "<a href=...>##{merge_request}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, the Integer merge request ID, and an optional
|
||||||
|
# String of the external project reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(MERGE_REQUEST_PATTERN) do |match|
|
||||||
|
yield match, $~[:merge_request].to_i, $~[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract `!123` merge request references from text
|
||||||
|
#
|
||||||
|
# This pattern supports cross-project references.
|
||||||
|
MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content|
|
||||||
|
merge_request_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace `!123` merge request references in text with links to the
|
||||||
|
# referenced merge request's details page.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with `!123` references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-merge_request` class names attached for styling.
|
||||||
|
def merge_request_link_filter(text)
|
||||||
|
self.class.references_in(text) do |match, id, project_ref|
|
||||||
|
project = self.project_from_ref(project_ref)
|
||||||
|
|
||||||
|
if project && merge_request = project.merge_requests.find_by(iid: id)
|
||||||
|
title = escape_once("Merge Request: #{merge_request.title}")
|
||||||
|
klass = reference_class(:merge_request)
|
||||||
|
|
||||||
|
url = url_for_merge_request(merge_request, project)
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{project_ref}!#{id}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): Cleanup
|
||||||
|
def url_for_merge_request(mr, project)
|
||||||
|
h = Rails.application.routes.url_helpers
|
||||||
|
h.namespace_project_merge_request_url(project.namespace, project, mr,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,76 @@
|
||||||
|
require 'active_support/core_ext/string/output_safety'
|
||||||
|
require 'html/pipeline'
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# Base class for GitLab Flavored Markdown reference filters.
|
||||||
|
#
|
||||||
|
# References within <pre>, <code>, <a>, and <style> elements are ignored.
|
||||||
|
#
|
||||||
|
# Context options:
|
||||||
|
# :project (required) - Current project, ignored if reference is cross-project.
|
||||||
|
# :reference_class - Custom CSS class added to reference links.
|
||||||
|
# :only_path - Generate path-only links.
|
||||||
|
#
|
||||||
|
class ReferenceFilter < HTML::Pipeline::Filter
|
||||||
|
def escape_once(html)
|
||||||
|
ERB::Util.html_escape_once(html)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Don't look for references in text nodes that are children of these
|
||||||
|
# elements.
|
||||||
|
IGNORE_PARENTS = %w(pre code a style).to_set
|
||||||
|
|
||||||
|
def ignored_ancestry?(node)
|
||||||
|
has_ancestor?(node, IGNORE_PARENTS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
context[:project]
|
||||||
|
end
|
||||||
|
|
||||||
|
def reference_class(type)
|
||||||
|
"gfm gfm-#{type} #{context[:reference_class]}".strip
|
||||||
|
end
|
||||||
|
|
||||||
|
# Iterate through the document's text nodes, yielding the current node's
|
||||||
|
# content if:
|
||||||
|
#
|
||||||
|
# * The `project` context value is present AND
|
||||||
|
# * The node's content matches `pattern` AND
|
||||||
|
# * The node is not an ancestor of an ignored node type
|
||||||
|
#
|
||||||
|
# pattern - Regex pattern against which to match the node's content
|
||||||
|
#
|
||||||
|
# Yields the current node's String contents. The result of the block will
|
||||||
|
# replace the node's existing content and update the current document.
|
||||||
|
#
|
||||||
|
# Returns the updated Nokogiri::XML::Document object.
|
||||||
|
def replace_text_nodes_matching(pattern)
|
||||||
|
return doc if project.nil?
|
||||||
|
|
||||||
|
doc.search('text()').each do |node|
|
||||||
|
content = node.to_html
|
||||||
|
|
||||||
|
next unless content.match(pattern)
|
||||||
|
next if ignored_ancestry?(node)
|
||||||
|
|
||||||
|
html = yield content
|
||||||
|
|
||||||
|
next if html == content
|
||||||
|
|
||||||
|
node.replace(html)
|
||||||
|
end
|
||||||
|
|
||||||
|
doc
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure that a :project key exists in context
|
||||||
|
#
|
||||||
|
# Note that while the key might exist, its value could be nil!
|
||||||
|
def validate
|
||||||
|
needs :project
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,72 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces snippet references with links. References to
|
||||||
|
# snippets that do not exist are ignored.
|
||||||
|
#
|
||||||
|
# This filter supports cross-project references.
|
||||||
|
class SnippetReferenceFilter < ReferenceFilter
|
||||||
|
include CrossProjectReference
|
||||||
|
|
||||||
|
# Public: Find `$123` snippet references in text
|
||||||
|
#
|
||||||
|
# SnippetReferenceFilter.references_in(text) do |match, snippet|
|
||||||
|
# "<a href=...>$#{snippet}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, the Integer snippet ID, and an optional String
|
||||||
|
# of the external project reference.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(SNIPPET_PATTERN) do |match|
|
||||||
|
yield match, $~[:snippet].to_i, $~[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract `$123` snippet references from text
|
||||||
|
#
|
||||||
|
# This pattern supports cross-project references.
|
||||||
|
SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(SNIPPET_PATTERN) do |content|
|
||||||
|
snippet_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace `$123` snippet references in text with links to the referenced
|
||||||
|
# snippets's details page.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with `$123` references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-snippet` class names attached for styling.
|
||||||
|
def snippet_link_filter(text)
|
||||||
|
self.class.references_in(text) do |match, id, project_ref|
|
||||||
|
project = self.project_from_ref(project_ref)
|
||||||
|
|
||||||
|
if project && snippet = project.snippets.find_by(id: id)
|
||||||
|
title = escape_once("Snippet: #{snippet.title}")
|
||||||
|
klass = reference_class(:snippet)
|
||||||
|
|
||||||
|
url = url_for_snippet(snippet, project)
|
||||||
|
|
||||||
|
%(<a href="#{url}"
|
||||||
|
title="#{title}"
|
||||||
|
class="#{klass}">#{project_ref}$#{id}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_for_snippet(snippet, project)
|
||||||
|
h = Rails.application.routes.url_helpers
|
||||||
|
h.namespace_project_snippet_url(project.namespace, project, snippet,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,92 @@
|
||||||
|
module Gitlab
|
||||||
|
module Markdown
|
||||||
|
# HTML filter that replaces user or group references with links.
|
||||||
|
#
|
||||||
|
# A special `@all` reference is also supported.
|
||||||
|
class UserReferenceFilter < ReferenceFilter
|
||||||
|
# Public: Find `@user` user references in text
|
||||||
|
#
|
||||||
|
# UserReferenceFilter.references_in(text) do |match, username|
|
||||||
|
# "<a href=...>@#{user}</a>"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# text - String text to search.
|
||||||
|
#
|
||||||
|
# Yields the String match, and the String user name.
|
||||||
|
#
|
||||||
|
# Returns a String replaced with the return of the block.
|
||||||
|
def self.references_in(text)
|
||||||
|
text.gsub(USER_PATTERN) do |match|
|
||||||
|
yield match, $~[:user]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Pattern used to extract `@user` user references from text
|
||||||
|
USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_text_nodes_matching(USER_PATTERN) do |content|
|
||||||
|
user_link_filter(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace `@user` user references in text with links to the referenced
|
||||||
|
# user's profile page.
|
||||||
|
#
|
||||||
|
# text - String text to replace references in.
|
||||||
|
#
|
||||||
|
# Returns a String with `@user` references replaced with links. All links
|
||||||
|
# have `gfm` and `gfm-project_member` class names attached for styling.
|
||||||
|
def user_link_filter(text)
|
||||||
|
project = context[:project]
|
||||||
|
|
||||||
|
self.class.references_in(text) do |match, user|
|
||||||
|
klass = reference_class(:project_member)
|
||||||
|
|
||||||
|
if user == 'all'
|
||||||
|
url = link_to_all(project)
|
||||||
|
|
||||||
|
%(<a href="#{url}" class="#{klass}">@#{user}</a>)
|
||||||
|
elsif namespace = Namespace.find_by(path: user)
|
||||||
|
if namespace.is_a?(Group)
|
||||||
|
if user_can_reference_group?(namespace)
|
||||||
|
url = group_url(user, only_path: context[:only_path])
|
||||||
|
%(<a href="#{url}" class="#{klass}">@#{user}</a>)
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
else
|
||||||
|
url = user_url(user, only_path: context[:only_path])
|
||||||
|
%(<a href="#{url}" class="#{klass}">@#{user}</a>)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def urls
|
||||||
|
Rails.application.routes.url_helpers
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_url(*args)
|
||||||
|
urls.group_url(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_url(*args)
|
||||||
|
urls.user_url(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_all(project)
|
||||||
|
urls.namespace_project_url(project.namespace, project,
|
||||||
|
only_path: context[:only_path])
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_can_reference_group?(group)
|
||||||
|
Ability.abilities.allowed?(context[:current_user], :read_group, group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,8 +3,6 @@ module Gitlab
|
||||||
class ReferenceExtractor
|
class ReferenceExtractor
|
||||||
attr_accessor :project, :current_user, :references
|
attr_accessor :project, :current_user, :references
|
||||||
|
|
||||||
include ::Gitlab::Markdown
|
|
||||||
|
|
||||||
def initialize(project, current_user = nil)
|
def initialize(project, current_user = nil)
|
||||||
@project = project
|
@project = project
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
|
@ -34,7 +32,7 @@ module Gitlab
|
||||||
project.team.members.flatten
|
project.team.members.flatten
|
||||||
elsif namespace = Namespace.find_by(path: identifier)
|
elsif namespace = Namespace.find_by(path: identifier)
|
||||||
if namespace.is_a?(Group)
|
if namespace.is_a?(Group)
|
||||||
namespace.users
|
namespace.users if can?(current_user, :read_group, namespace)
|
||||||
else
|
else
|
||||||
namespace.owner
|
namespace.owner
|
||||||
end
|
end
|
||||||
|
@ -87,6 +85,72 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
|
||||||
|
PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
|
||||||
|
|
||||||
|
REFERENCE_PATTERN = %r{
|
||||||
|
(?<prefix>\W)? # Prefix
|
||||||
|
( # Reference
|
||||||
|
@(?<user>#{NAME_STR}) # User name
|
||||||
|
|~(?<label>\d+) # Label ID
|
||||||
|
|(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
|
||||||
|
|#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
|
||||||
|
|#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
|
||||||
|
|\$(?<snippet>\d+) # Snippet ID
|
||||||
|
|(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
|
||||||
|
|(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
|
||||||
|
)
|
||||||
|
(?<suffix>\W)? # Suffix
|
||||||
|
}x.freeze
|
||||||
|
|
||||||
|
TYPES = %i(user issue label merge_request snippet commit commit_range).freeze
|
||||||
|
|
||||||
|
def parse_references(text, project = @project)
|
||||||
|
# parse reference links
|
||||||
|
text.gsub!(REFERENCE_PATTERN) do |match|
|
||||||
|
type = TYPES.detect { |t| $~[t].present? }
|
||||||
|
|
||||||
|
actual_project = project
|
||||||
|
project_prefix = nil
|
||||||
|
project_path = $LAST_MATCH_INFO[:project]
|
||||||
|
if project_path
|
||||||
|
actual_project = ::Project.find_with_namespace(project_path)
|
||||||
|
actual_project = nil unless can?(current_user, :read_project, actual_project)
|
||||||
|
project_prefix = project_path
|
||||||
|
end
|
||||||
|
|
||||||
|
parse_result($LAST_MATCH_INFO, type,
|
||||||
|
actual_project, project_prefix) || match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Called from #parse_references. Attempts to build a gitlab reference
|
||||||
|
# link. Returns nil if +type+ is nil, if the match string is an HTML
|
||||||
|
# entity, if the reference is invalid, or if the matched text includes an
|
||||||
|
# invalid project path.
|
||||||
|
def parse_result(match_info, type, project, project_prefix)
|
||||||
|
prefix = match_info[:prefix]
|
||||||
|
suffix = match_info[:suffix]
|
||||||
|
|
||||||
|
return nil if html_entity?(prefix, suffix) || type.nil?
|
||||||
|
return nil if project.nil? && !project_prefix.nil?
|
||||||
|
|
||||||
|
identifier = match_info[type]
|
||||||
|
ref_link = reference_link(type, identifier, project, project_prefix)
|
||||||
|
|
||||||
|
if ref_link
|
||||||
|
"#{prefix}#{ref_link}#{suffix}"
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return true if the +prefix+ and +suffix+ indicate that the matched string
|
||||||
|
# is an HTML entity like &
|
||||||
|
def html_entity?(prefix, suffix)
|
||||||
|
prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
|
||||||
|
end
|
||||||
|
|
||||||
def reference_link(type, identifier, project, _)
|
def reference_link(type, identifier, project, _)
|
||||||
references[type] << [project, identifier]
|
references[type] << [project, identifier]
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,417 +36,11 @@ describe GitlabMarkdownHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#gfm" do
|
describe "#gfm" do
|
||||||
it "should return unaltered text if project is nil" do
|
|
||||||
actual = "Testing references: ##{issue.iid}"
|
|
||||||
|
|
||||||
expect(gfm(actual)).not_to eq(actual)
|
|
||||||
|
|
||||||
@project = nil
|
|
||||||
expect(gfm(actual)).to eq(actual)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not alter non-references" do
|
|
||||||
actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do."
|
|
||||||
expect(gfm(actual)).to eq(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not touch HTML entities" do
|
|
||||||
allow(@project.issues).to receive(:where).
|
|
||||||
with(id: '39').and_return([issue])
|
|
||||||
actual = 'We'll accept good pull requests.'
|
|
||||||
expect(gfm(actual)).to eq("We'll accept good pull requests.")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should forward HTML options to links" do
|
it "should forward HTML options to links" do
|
||||||
expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')).
|
expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')).
|
||||||
to have_selector('a.gfm.foo')
|
to have_selector('a.gfm.foo')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "referencing a commit range" do
|
|
||||||
let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) }
|
|
||||||
|
|
||||||
it "should link using a full id" do
|
|
||||||
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using a short id" do
|
|
||||||
actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}"
|
|
||||||
expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id)
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link inclusively" do
|
|
||||||
actual = "What happened in #{earlier_commit.id}..#{commit.id}"
|
|
||||||
expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id)
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
actual = "(see #{earlier_commit.id}...#{commit.id})"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically"
|
|
||||||
expected = /Changes <a.+>#{earlier_commit.id}...#{commit.id}<\/a> dramatically/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}"
|
|
||||||
expect(gfm(actual)).to eq(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include a title attribute" do
|
|
||||||
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing a commit" do
|
|
||||||
let(:expected) { namespace_project_commit_path(project.namespace, project, commit) }
|
|
||||||
|
|
||||||
it "should link using a full id" do
|
|
||||||
actual = "Reverts #{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using a short id" do
|
|
||||||
actual = "Backported from #{commit.short_id}"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
actual = "Reverted (see #{commit.id})"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Changes #{commit.id} dramatically"
|
|
||||||
expected = /Changes <a.+>#{commit.id}<\/a> dramatically/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
actual = expected = "What happened in #{commit.id.reverse}"
|
|
||||||
expect(gfm(actual)).to eq(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include a title attribute" do
|
|
||||||
actual = "Reverts #{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(/title="#{commit.link_title}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
actual = "Reverts #{commit.id}"
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-commit\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing a team member" do
|
|
||||||
let(:actual) { "@#{user.username} you are right." }
|
|
||||||
let(:expected) { user_path(user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.team << [user, :master]
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using a simple name" do
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using a name with dots" do
|
|
||||||
user.update_attributes(name: "alphA.Beta")
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using name with underscores" do
|
|
||||||
user.update_attributes(name: "ping_pong_king")
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
actual = "Mail the admin (@#{user.username})"
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Yes, @#{user.username} is right."
|
|
||||||
expected = /Yes, <a.+>@#{user.username}<\/a> is right/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
actual = expected = "@#{user.username.reverse} you are right."
|
|
||||||
expect(gfm(actual)).to eq(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-project_member\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Shared examples for referencing an object
|
|
||||||
#
|
|
||||||
# Expects the following attributes to be available in the example group:
|
|
||||||
#
|
|
||||||
# - object - The object itself
|
|
||||||
# - reference - The object reference string (e.g., #1234, $1234, !1234)
|
|
||||||
#
|
|
||||||
# Currently limited to Snippets, Issues and MergeRequests
|
|
||||||
shared_examples 'referenced object' do
|
|
||||||
let(:actual) { "Reference to #{reference}" }
|
|
||||||
let(:expected) { polymorphic_path([project.namespace, project, object]) }
|
|
||||||
|
|
||||||
it "should link using a valid id" do
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
# Wrap the reference in parenthesis
|
|
||||||
expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
|
|
||||||
|
|
||||||
# Append some text to the end of the reference
|
|
||||||
expect(gfm(actual.gsub(reference, "#{reference}, right?"))).
|
|
||||||
to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Referenced #{reference} already."
|
|
||||||
expected = /Referenced <a.+>[^\s]+<\/a> already/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
# Modify the reference string so it's still parsed, but is invalid
|
|
||||||
reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
|
|
||||||
expect(gfm(actual)).to eq(actual)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include a title attribute" do
|
|
||||||
title = "#{object.class.to_s.titlecase}: #{object.title}"
|
|
||||||
expect(gfm(actual)).to match(/title="#{title}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
css = object.class.to_s.underscore
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Shared examples for referencing an object in a different project
|
|
||||||
#
|
|
||||||
# Expects the following attributes to be available in the example group:
|
|
||||||
#
|
|
||||||
# - object - The object itself
|
|
||||||
# - reference - The object reference string (e.g., #1234, $1234, !1234)
|
|
||||||
# - other_project - The project that owns the target object
|
|
||||||
#
|
|
||||||
# Currently limited to Snippets, Issues and MergeRequests
|
|
||||||
shared_examples 'cross-project referenced object' do
|
|
||||||
let(:project_path) { @other_project.path_with_namespace }
|
|
||||||
let(:full_reference) { "#{project_path}#{reference}" }
|
|
||||||
let(:actual) { "Reference to #{full_reference}" }
|
|
||||||
let(:expected) do
|
|
||||||
if object.is_a?(Commit)
|
|
||||||
namespace_project_commit_path(@other_project.namespace, @other_project, object)
|
|
||||||
else
|
|
||||||
polymorphic_path([@other_project.namespace, @other_project, object])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should link using a valid id' do
|
|
||||||
expect(gfm(actual)).to match(
|
|
||||||
/#{expected}.*#{Regexp.escape(full_reference)}/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should link with adjacent text' do
|
|
||||||
# Wrap the reference in parenthesis
|
|
||||||
expect(gfm(actual.gsub(full_reference, "(#{full_reference})"))).to(
|
|
||||||
match(expected)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Append some text to the end of the reference
|
|
||||||
expect(gfm(actual.gsub(full_reference, "#{full_reference}, right?"))).
|
|
||||||
to(match(expected))
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should keep whitespace intact' do
|
|
||||||
actual = "Referenced #{full_reference} already."
|
|
||||||
expected = /Referenced <a.+>[^\s]+<\/a> already/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should not link with an invalid id' do
|
|
||||||
# Modify the reference string so it's still parsed, but is invalid
|
|
||||||
if object.is_a?(Commit)
|
|
||||||
reference.gsub!(/^(.).+$/, '\1' + '12345abcd')
|
|
||||||
else
|
|
||||||
reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
|
|
||||||
end
|
|
||||||
expect(gfm(actual)).to eq(actual)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should include a title attribute' do
|
|
||||||
if object.is_a?(Commit)
|
|
||||||
title = object.link_title
|
|
||||||
else
|
|
||||||
title = "#{object.class.to_s.titlecase}: #{object.title}"
|
|
||||||
end
|
|
||||||
expect(gfm(actual)).to match(/title="#{title}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should include standard gfm classes' do
|
|
||||||
css = object.class.to_s.underscore
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing an issue" do
|
|
||||||
let(:object) { issue }
|
|
||||||
let(:reference) { "##{issue.iid}" }
|
|
||||||
|
|
||||||
include_examples 'referenced object'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'cross-repo references' do
|
|
||||||
before(:all) do
|
|
||||||
@other_project = create(:project, :public)
|
|
||||||
@commit2 = @other_project.repository.commit
|
|
||||||
@issue2 = create(:issue, project: @other_project)
|
|
||||||
@merge_request2 = create(:merge_request,
|
|
||||||
source_project: @other_project,
|
|
||||||
target_project: @other_project)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'referencing an issue in another project' do
|
|
||||||
let(:object) { @issue2 }
|
|
||||||
let(:reference) { "##{@issue2.iid}" }
|
|
||||||
|
|
||||||
include_examples 'cross-project referenced object'
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'referencing an merge request in another project' do
|
|
||||||
let(:object) { @merge_request2 }
|
|
||||||
let(:reference) { "!#{@merge_request2.iid}" }
|
|
||||||
|
|
||||||
include_examples 'cross-project referenced object'
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'referencing a commit in another project' do
|
|
||||||
let(:object) { @commit2 }
|
|
||||||
let(:reference) { "@#{@commit2.id}" }
|
|
||||||
|
|
||||||
include_examples 'cross-project referenced object'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing a Jira issue" do
|
|
||||||
let(:actual) { "Reference to JIRA-#{issue.iid}" }
|
|
||||||
let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" }
|
|
||||||
let(:reference) { "JIRA-#{issue.iid}" }
|
|
||||||
|
|
||||||
before do
|
|
||||||
jira = @project.create_jira_service if @project.jira_service.nil?
|
|
||||||
properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"}
|
|
||||||
jira.update_attributes(properties: properties, active: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
@project.jira_service.destroy! unless @project.jira_service.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link using a valid id" do
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
# Wrap the reference in parenthesis
|
|
||||||
expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
|
|
||||||
|
|
||||||
# Append some text to the end of the reference
|
|
||||||
expect(gfm(actual.gsub(reference, "#{reference}, right?"))).
|
|
||||||
to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Referenced #{reference} already."
|
|
||||||
expected = /Referenced <a.+>[^\s]+<\/a> already/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
# Modify the reference string so it's still parsed, but is invalid
|
|
||||||
invalid_reference = actual.gsub(/(\d+)$/, "r45")
|
|
||||||
expect(gfm(invalid_reference)).to eq(invalid_reference)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include a title attribute" do
|
|
||||||
title = "Issue in JIRA tracker"
|
|
||||||
expect(gfm(actual)).to match(/title="#{title}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-issue\s?"/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing a merge request" do
|
|
||||||
let(:object) { merge_request }
|
|
||||||
let(:reference) { "!#{merge_request.iid}" }
|
|
||||||
|
|
||||||
include_examples 'referenced object'
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "referencing a snippet" do
|
|
||||||
let(:object) { snippet }
|
|
||||||
let(:reference) { "$#{snippet.id}" }
|
|
||||||
let(:actual) { "Reference to #{reference}" }
|
|
||||||
let(:expected) { namespace_project_snippet_path(project.namespace, project, object) }
|
|
||||||
|
|
||||||
it "should link using a valid id" do
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should link with adjacent text" do
|
|
||||||
# Wrap the reference in parenthesis
|
|
||||||
expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
|
|
||||||
|
|
||||||
# Append some text to the end of the reference
|
|
||||||
expect(gfm(actual.gsub(reference, "#{reference}, right?"))).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should keep whitespace intact" do
|
|
||||||
actual = "Referenced #{reference} already."
|
|
||||||
expected = /Referenced <a.+>[^\s]+<\/a> already/
|
|
||||||
expect(gfm(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link with an invalid id" do
|
|
||||||
# Modify the reference string so it's still parsed, but is invalid
|
|
||||||
reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
|
|
||||||
expect(gfm(actual)).to eq(actual)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include a title attribute" do
|
|
||||||
title = "Snippet: #{object.title}"
|
|
||||||
expect(gfm(actual)).to match(/title="#{title}"/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should include standard gfm classes" do
|
|
||||||
css = object.class.to_s.underscore
|
|
||||||
expect(gfm(actual)).to match(/class="\s?gfm gfm-snippet\s?"/)
|
|
||||||
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.iid} -> #{commit.id} -> ##{issue.iid}" }
|
||||||
|
|
||||||
|
@ -466,6 +60,7 @@ describe GitlabMarkdownHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): These tests belong in the emoji filter spec
|
||||||
describe "emoji" do
|
describe "emoji" do
|
||||||
it "matches at the start of a string" do
|
it "matches at the start of a string" do
|
||||||
expect(gfm(":+1:")).to match(/<img/)
|
expect(gfm(":+1:")).to match(/<img/)
|
||||||
|
@ -511,6 +106,116 @@ describe GitlabMarkdownHelper do
|
||||||
expect(gfm(":+1:")).to match(/<img/)
|
expect(gfm(":+1:")).to match(/<img/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'parse_tasks: true' do
|
||||||
|
before(:all) do
|
||||||
|
@source_text_asterisk = <<-EOT.strip_heredoc
|
||||||
|
* [ ] valid unchecked task
|
||||||
|
* [x] valid lowercase checked task
|
||||||
|
* [X] valid uppercase checked task
|
||||||
|
* [ ] valid unchecked nested task
|
||||||
|
* [x] valid checked nested task
|
||||||
|
|
||||||
|
[ ] not an unchecked task - no list item
|
||||||
|
[x] not a checked task - no list item
|
||||||
|
|
||||||
|
* [ ] not an unchecked task - too many spaces
|
||||||
|
* [x ] not a checked task - too many spaces
|
||||||
|
* [] not an unchecked task - no spaces
|
||||||
|
* Not a task [ ] - not at beginning
|
||||||
|
EOT
|
||||||
|
|
||||||
|
@source_text_dash = <<-EOT.strip_heredoc
|
||||||
|
- [ ] valid unchecked task
|
||||||
|
- [x] valid lowercase checked task
|
||||||
|
- [X] valid uppercase checked task
|
||||||
|
- [ ] valid unchecked nested task
|
||||||
|
- [x] valid checked nested task
|
||||||
|
EOT
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should render checkboxes at beginning of asterisk list items' do
|
||||||
|
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid lowercase checked task/
|
||||||
|
)
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid uppercase checked task/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should render checkboxes at beginning of dash list items' do
|
||||||
|
rendered_text = markdown(@source_text_dash, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid lowercase checked task/
|
||||||
|
)
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid uppercase checked task/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should render checkboxes for nested tasks' do
|
||||||
|
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid unchecked nested task/
|
||||||
|
)
|
||||||
|
expect(rendered_text).to match(
|
||||||
|
/<input.*checkbox.*valid checked nested task/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be confused by whitespace before bullets' do
|
||||||
|
rendered_text_asterisk = markdown(@source_text_asterisk,
|
||||||
|
parse_tasks: true)
|
||||||
|
rendered_text_dash = markdown(@source_text_dash, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text_asterisk).to match(
|
||||||
|
/<input.*checkbox.*valid unchecked nested task/
|
||||||
|
)
|
||||||
|
expect(rendered_text_asterisk).to match(
|
||||||
|
/<input.*checkbox.*valid checked nested task/
|
||||||
|
)
|
||||||
|
expect(rendered_text_dash).to match(
|
||||||
|
/<input.*checkbox.*valid unchecked nested task/
|
||||||
|
)
|
||||||
|
expect(rendered_text_dash).to match(
|
||||||
|
/<input.*checkbox.*valid checked nested task/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not render checkboxes outside of list items' do
|
||||||
|
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/<input.*checkbox.*not an unchecked task - no list item/
|
||||||
|
)
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/<input.*checkbox.*not a checked task - no list item/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not render checkboxes with invalid formatting' do
|
||||||
|
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
||||||
|
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/<input.*checkbox.*not an unchecked task - too many spaces/
|
||||||
|
)
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/<input.*checkbox.*not a checked task - too many spaces/
|
||||||
|
)
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/<input.*checkbox.*not an unchecked task - no spaces/
|
||||||
|
)
|
||||||
|
expect(rendered_text).not_to match(
|
||||||
|
/Not a task.*<input.*checkbox.*not at beginning/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#link_to_gfm" do
|
describe "#link_to_gfm" do
|
||||||
|
@ -560,11 +265,9 @@ describe GitlabMarkdownHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#markdown" do
|
describe "#markdown" do
|
||||||
it "should handle references in paragraphs" do
|
# TODO (rspeicher) - This block tests multiple different contexts. Break this up!
|
||||||
actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n"
|
|
||||||
expected = namespace_project_commit_path(project.namespace, project, commit)
|
# REFERENCES (PART TWO: THE REVENGE) ---------------------------------------
|
||||||
expect(markdown(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should handle references in headers" do
|
it "should handle references in headers" do
|
||||||
actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}"
|
actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}"
|
||||||
|
@ -590,37 +293,6 @@ describe GitlabMarkdownHelper do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should handle references in lists" do
|
|
||||||
project.team << [user, :master]
|
|
||||||
|
|
||||||
actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}"
|
|
||||||
|
|
||||||
expect(markdown(actual)).
|
|
||||||
to match(%r{<li>dark: <a.+>##{issue.iid}</a></li>})
|
|
||||||
expect(markdown(actual)).
|
|
||||||
to match(%r{<li>light by <a.+>@#{member.user.username}</a></li>})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link the apostrophe to issue 39" do
|
|
||||||
project.team << [user, :master]
|
|
||||||
allow(project.issues).
|
|
||||||
to receive(:where).with(iid: '39').and_return([issue])
|
|
||||||
|
|
||||||
actual = "Yes, it is @#{member.user.username}'s task."
|
|
||||||
expected = /Yes, it is <a.+>@#{member.user.username}<\/a>'s task/
|
|
||||||
expect(markdown(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should not link the apostrophe to issue 39 in code blocks" do
|
|
||||||
project.team << [user, :master]
|
|
||||||
allow(project.issues).
|
|
||||||
to receive(:where).with(iid: '39').and_return([issue])
|
|
||||||
|
|
||||||
actual = "Yes, `it is @#{member.user.username}'s task.`"
|
|
||||||
expected = /Yes, <code>it is @gfm\'s task.<\/code>/
|
|
||||||
expect(markdown(actual)).to match(expected)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should handle references in <em>" do
|
it "should handle references in <em>" do
|
||||||
actual = "Apply _!#{merge_request.iid}_ ASAP"
|
actual = "Apply _!#{merge_request.iid}_ ASAP"
|
||||||
|
|
||||||
|
@ -628,16 +300,10 @@ describe GitlabMarkdownHelper do
|
||||||
to match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>})
|
to match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>})
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should handle tables" do
|
# CODE BLOCKS -------------------------------------------------------------
|
||||||
actual = %Q{| header 1 | header 2 |
|
|
||||||
| -------- | -------- |
|
|
||||||
| cell 1 | cell 2 |
|
|
||||||
| cell 3 | cell 4 |}
|
|
||||||
|
|
||||||
expect(markdown(actual)).to match(/\A<table/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should leave code blocks untouched" do
|
it "should leave code blocks untouched" do
|
||||||
|
allow(helper).to receive(:current_user).and_return(user)
|
||||||
allow(helper).to receive(:user_color_scheme_class).and_return(:white)
|
allow(helper).to receive(:user_color_scheme_class).and_return(:white)
|
||||||
|
|
||||||
target_html = "<pre class=\"code highlight white plaintext\"><code>some code from $#{snippet.id}\nhere too\n</code></pre>\n"
|
target_html = "<pre class=\"code highlight white plaintext\"><code>some code from $#{snippet.id}\nhere too\n</code></pre>\n"
|
||||||
|
@ -654,12 +320,15 @@ describe GitlabMarkdownHelper do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# REF-LIKE AUTOLINKS? -----------------------------------------------------
|
||||||
|
# Basically: Don't parse references inside `<a>` tags.
|
||||||
|
|
||||||
it "should leave ref-like autolinks untouched" do
|
it "should leave ref-like autolinks untouched" do
|
||||||
expect(markdown("look at http://example.tld/#!#{merge_request.iid}")).to eq("<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n")
|
expect(markdown("look at http://example.tld/#!#{merge_request.iid}")).to eq("<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should leave ref-like href of 'manual' links untouched" do
|
it "should leave ref-like href of 'manual' links untouched" do
|
||||||
expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
|
expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\" class=\"gfm gfm-merge_request\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should leave ref-like src of images untouched" do
|
it "should leave ref-like src of images untouched" do
|
||||||
|
@ -670,23 +339,30 @@ describe GitlabMarkdownHelper do
|
||||||
expect(markdown("##{issue.iid}")).to include(namespace_project_issue_path(project.namespace, project, issue))
|
expect(markdown("##{issue.iid}")).to include(namespace_project_issue_path(project.namespace, project, issue))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# EMOJI -------------------------------------------------------------------
|
||||||
|
|
||||||
it "should generate absolute urls for emoji" do
|
it "should generate absolute urls for emoji" do
|
||||||
|
# TODO (rspeicher): Why isn't this with the emoji tests?
|
||||||
expect(markdown(':smile:')).to(
|
expect(markdown(':smile:')).to(
|
||||||
include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/#{Emoji.emoji_filename('smile')}.png))
|
include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/#{Emoji.emoji_filename('smile')}.png))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should generate absolute urls for emoji if relative url is present" do
|
it "should generate absolute urls for emoji if relative url is present" do
|
||||||
|
# TODO (rspeicher): Why isn't this with the emoji tests?
|
||||||
allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root')
|
allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root')
|
||||||
expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/#{Emoji.emoji_filename('smile')}.png")
|
expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/#{Emoji.emoji_filename('smile')}.png")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should generate absolute urls for emoji if asset_host is present" do
|
it "should generate absolute urls for emoji if asset_host is present" do
|
||||||
|
# TODO (rspeicher): Why isn't this with the emoji tests?
|
||||||
allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com")
|
allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com")
|
||||||
ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com")
|
ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com")
|
||||||
expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/#{Emoji.emoji_filename('smile')}.png")
|
expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/#{Emoji.emoji_filename('smile')}.png")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# RELATIVE URLS -----------------------------------------------------------
|
||||||
|
# TODO (rspeicher): These belong in a relative link filter spec
|
||||||
|
|
||||||
it "should handle relative urls for a file in master" do
|
it "should handle relative urls for a file in master" do
|
||||||
actual = "[GitLab API doc](doc/api/README.md)\n"
|
actual = "[GitLab API doc](doc/api/README.md)\n"
|
||||||
|
@ -741,6 +417,8 @@ describe GitlabMarkdownHelper do
|
||||||
expect(markdown(actual)).to match(actual)
|
expect(markdown(actual)).to match(actual)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# SANITIZATION ------------------------------------------------------------
|
||||||
|
|
||||||
it 'should sanitize tags that are not whitelisted' do
|
it 'should sanitize tags that are not whitelisted' do
|
||||||
actual = '<textarea>no inputs allowed</textarea> <blink>no blinks</blink>'
|
actual = '<textarea>no inputs allowed</textarea> <blink>no blinks</blink>'
|
||||||
expected = 'no inputs allowed no blinks'
|
expected = 'no inputs allowed no blinks'
|
||||||
|
@ -767,6 +445,7 @@ describe GitlabMarkdownHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): This should be a context of relative link specs, not its own thing
|
||||||
describe 'markdown for empty repository' do
|
describe 'markdown for empty repository' do
|
||||||
before do
|
before do
|
||||||
@project = empty_project
|
@project = empty_project
|
||||||
|
@ -780,7 +459,7 @@ describe GitlabMarkdownHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#render_wiki_content" do
|
describe '#render_wiki_content' do
|
||||||
before do
|
before do
|
||||||
@wiki = double('WikiPage')
|
@wiki = double('WikiPage')
|
||||||
allow(@wiki).to receive(:content).and_return('wiki content')
|
allow(@wiki).to receive(:content).and_return('wiki content')
|
||||||
|
@ -803,114 +482,4 @@ describe GitlabMarkdownHelper do
|
||||||
helper.render_wiki_content(@wiki)
|
helper.render_wiki_content(@wiki)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#gfm_with_tasks' do
|
|
||||||
before(:all) do
|
|
||||||
@source_text_asterisk = <<EOT.gsub(/^\s{8}/, '')
|
|
||||||
* [ ] valid unchecked task
|
|
||||||
* [x] valid lowercase checked task
|
|
||||||
* [X] valid uppercase checked task
|
|
||||||
* [ ] valid unchecked nested task
|
|
||||||
* [x] valid checked nested task
|
|
||||||
|
|
||||||
[ ] not an unchecked task - no list item
|
|
||||||
[x] not a checked task - no list item
|
|
||||||
|
|
||||||
* [ ] not an unchecked task - too many spaces
|
|
||||||
* [x ] not a checked task - too many spaces
|
|
||||||
* [] not an unchecked task - no spaces
|
|
||||||
* Not a task [ ] - not at beginning
|
|
||||||
EOT
|
|
||||||
|
|
||||||
@source_text_dash = <<EOT.gsub(/^\s{8}/, '')
|
|
||||||
- [ ] valid unchecked task
|
|
||||||
- [x] valid lowercase checked task
|
|
||||||
- [X] valid uppercase checked task
|
|
||||||
- [ ] valid unchecked nested task
|
|
||||||
- [x] valid checked nested task
|
|
||||||
EOT
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should render checkboxes at beginning of asterisk list items' do
|
|
||||||
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid lowercase checked task/
|
|
||||||
)
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid uppercase checked task/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should render checkboxes at beginning of dash list items' do
|
|
||||||
rendered_text = markdown(@source_text_dash, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid lowercase checked task/
|
|
||||||
)
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid uppercase checked task/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should render checkboxes for nested tasks' do
|
|
||||||
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid unchecked nested task/
|
|
||||||
)
|
|
||||||
expect(rendered_text).to match(
|
|
||||||
/<input.*checkbox.*valid checked nested task/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should not be confused by whitespace before bullets' do
|
|
||||||
rendered_text_asterisk = markdown(@source_text_asterisk,
|
|
||||||
parse_tasks: true)
|
|
||||||
rendered_text_dash = markdown(@source_text_dash, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text_asterisk).to match(
|
|
||||||
/<input.*checkbox.*valid unchecked nested task/
|
|
||||||
)
|
|
||||||
expect(rendered_text_asterisk).to match(
|
|
||||||
/<input.*checkbox.*valid checked nested task/
|
|
||||||
)
|
|
||||||
expect(rendered_text_dash).to match(
|
|
||||||
/<input.*checkbox.*valid unchecked nested task/
|
|
||||||
)
|
|
||||||
expect(rendered_text_dash).to match(
|
|
||||||
/<input.*checkbox.*valid checked nested task/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should not render checkboxes outside of list items' do
|
|
||||||
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/<input.*checkbox.*not an unchecked task - no list item/
|
|
||||||
)
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/<input.*checkbox.*not a checked task - no list item/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should not render checkboxes with invalid formatting' do
|
|
||||||
rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
|
|
||||||
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/<input.*checkbox.*not an unchecked task - too many spaces/
|
|
||||||
)
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/<input.*checkbox.*not a checked task - too many spaces/
|
|
||||||
)
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/<input.*checkbox.*not an unchecked task - no spaces/
|
|
||||||
)
|
|
||||||
expect(rendered_text).not_to match(
|
|
||||||
/Not a task.*<input.*checkbox.*not at beginning/
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe LabelsHelper do
|
describe LabelsHelper do
|
||||||
it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') }
|
it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') }
|
||||||
it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') }
|
it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') }
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe CommitRangeReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:commit1) { project.repository.commit }
|
||||||
|
let(:commit2) { project.repository.commit("HEAD~2") }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Commit Range 1c002d..d200c1', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'internal reference' do
|
||||||
|
let(:reference) { "#{commit1.id}...#{commit2.id}" }
|
||||||
|
let(:reference2) { "#{commit1.id}..#{commit2.id}" }
|
||||||
|
|
||||||
|
it 'links to a valid two-dot reference' do
|
||||||
|
doc = filter("See #{reference2}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to a valid three-dot reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to a valid short ID' do
|
||||||
|
reference = "#{commit1.short_id}...#{commit2.id}"
|
||||||
|
reference2 = "#{commit1.id}...#{commit2.short_id}"
|
||||||
|
|
||||||
|
expect(filter("See #{reference}").css('a').first.text).to eq reference
|
||||||
|
expect(filter("See #{reference2}").css('a').first.text).to eq reference2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("See (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid commit IDs' do
|
||||||
|
exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
|
||||||
|
|
||||||
|
expect(project).to receive(:valid_repo?).and_return(true)
|
||||||
|
expect(project.repository).to receive(:commit).with(commit1.id.reverse)
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("See #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path option' do
|
||||||
|
doc = filter("See #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cross-project reference' do
|
||||||
|
let(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||||
|
let(:project2) { create(:project, namespace: namespace) }
|
||||||
|
let(:commit1) { project.repository.commit }
|
||||||
|
let(:commit2) { project.repository.commit("HEAD~2") }
|
||||||
|
let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
|
||||||
|
|
||||||
|
context 'when user can access reference' do
|
||||||
|
before { allow_cross_reference! }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Fixed (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid commit IDs on the referenced project' do
|
||||||
|
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
|
||||||
|
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access reference' do
|
||||||
|
before { disallow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references' do
|
||||||
|
exp = act = "See #{reference}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,118 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe CommitReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:commit) { project.repository.commit }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Commit 1c002d', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'internal reference' do
|
||||||
|
let(:reference) { commit.id }
|
||||||
|
|
||||||
|
# Let's test a variety of commit SHA sizes just to be paranoid
|
||||||
|
[6, 8, 12, 18, 20, 32, 40].each do |size|
|
||||||
|
it "links to a valid reference of #{size} characters" do
|
||||||
|
doc = filter("See #{reference[0...size]}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.text).to eq reference[0...size]
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_commit_url(project.namespace, project, reference)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("See (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid commit IDs' do
|
||||||
|
exp = act = "See #{reference.reverse}"
|
||||||
|
|
||||||
|
expect(project).to receive(:valid_repo?).and_return(true)
|
||||||
|
expect(project.repository).to receive(:commit).with(reference.reverse)
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq commit.link_title
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the title attribute' do
|
||||||
|
allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
|
||||||
|
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
expect(doc.text).to eq "See #{commit.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("See #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("See #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cross-project reference' do
|
||||||
|
let(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||||
|
let(:project2) { create(:project, namespace: namespace) }
|
||||||
|
let(:commit) { project.repository.commit }
|
||||||
|
let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
|
||||||
|
|
||||||
|
context 'when user can access reference' do
|
||||||
|
before { allow_cross_reference! }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Fixed (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid commit IDs on the referenced project' do
|
||||||
|
exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access reference' do
|
||||||
|
before { disallow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references' do
|
||||||
|
exp = act = "See #{reference}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,56 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe CrossProjectReference do
|
||||||
|
# context in the html-pipeline sense, not in the rspec sense
|
||||||
|
let(:context) do
|
||||||
|
{
|
||||||
|
current_user: double('user'),
|
||||||
|
project: double('project')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
include described_class
|
||||||
|
|
||||||
|
describe '#project_from_ref' do
|
||||||
|
context 'when no project was referenced' do
|
||||||
|
it 'returns the project from context' do
|
||||||
|
expect(project_from_ref(nil)).to eq context[:project]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when referenced project does not exist' do
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(project_from_ref('invalid/reference')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when referenced project exists' do
|
||||||
|
let(:project2) { double('referenced project') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
expect(Project).to receive(:find_with_namespace).
|
||||||
|
with('cross/reference').and_return(project2)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the user has permission to read it' do
|
||||||
|
it 'returns the referenced project' do
|
||||||
|
expect(self).to receive(:user_can_reference_project?).
|
||||||
|
with(project2).and_return(true)
|
||||||
|
|
||||||
|
expect(project_from_ref('cross/reference')).to eq project2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the user does not have permission to read it' do
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(self).to receive(:user_can_reference_project?).
|
||||||
|
with(project2).and_return(false)
|
||||||
|
|
||||||
|
expect(project_from_ref('cross/reference')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,109 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe ExternalIssueReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
def helper
|
||||||
|
IssuesHelper
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:issue) { double('issue', iid: 123) }
|
||||||
|
|
||||||
|
context 'JIRA issue references' do
|
||||||
|
let(:reference) { "JIRA-#{issue.iid}" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
jira = project.create_jira_service
|
||||||
|
|
||||||
|
props = {
|
||||||
|
'title' => 'JIRA tracker',
|
||||||
|
'project_url' => 'http://jira.example/issues/?jql=project=A',
|
||||||
|
'issues_url' => 'http://jira.example/browse/:id',
|
||||||
|
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
|
||||||
|
}
|
||||||
|
|
||||||
|
jira.update_attributes(properties: props, active: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
project.jira_service.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Issue JIRA-123', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores valid references when using default tracker' do
|
||||||
|
expect(project).to receive(:default_issues_tracker?).and_return(true)
|
||||||
|
|
||||||
|
exp = act = "Issue #{reference}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('href'))
|
||||||
|
.to eq helper.url_for_issue(reference, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to the external tracker' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).to eq "http://jira.example/browse/#{reference}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Issue (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the title attribute' do
|
||||||
|
allow(project.external_issue_tracker).to receive(:title).
|
||||||
|
and_return(%{"></a>whatever<a title="})
|
||||||
|
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.text).to eq "Issue #{reference}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Issue #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Issue #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,133 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe IssueReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
def helper
|
||||||
|
IssuesHelper
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Issue #123', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'internal reference' do
|
||||||
|
let(:reference) { "##{issue.iid}" }
|
||||||
|
|
||||||
|
it 'ignores valid references when using non-default tracker' do
|
||||||
|
expect(project).to receive(:issue_exists?).with(issue.iid).and_return(false)
|
||||||
|
|
||||||
|
exp = act = "Issue ##{issue.iid}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq helper.url_for_issue(issue.iid, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Fixed (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid issue IDs' do
|
||||||
|
exp = act = "Fixed ##{issue.iid + 1}"
|
||||||
|
|
||||||
|
expect(project).to receive(:issue_exists?).with(issue.iid + 1)
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the title attribute' do
|
||||||
|
issue.update_attribute(:title, %{"></a>whatever<a title="})
|
||||||
|
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.text).to eq "Issue #{reference}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Issue #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Issue #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Issue #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cross-project reference' do
|
||||||
|
let(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||||
|
let(:project2) { create(:empty_project, namespace: namespace) }
|
||||||
|
let(:issue) { create(:issue, project: project2) }
|
||||||
|
let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
|
||||||
|
|
||||||
|
context 'when user can access reference' do
|
||||||
|
before { allow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references when cross-reference project uses external tracker' do
|
||||||
|
expect_any_instance_of(Project).to receive(:issue_exists?).
|
||||||
|
with(issue.iid).and_return(false)
|
||||||
|
|
||||||
|
exp = act = "Issue ##{issue.iid}"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq helper.url_for_issue(issue.iid, project2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Fixed (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid issue IDs on the referenced project' do
|
||||||
|
exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access reference' do
|
||||||
|
before { disallow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references' do
|
||||||
|
exp = act = "See #{reference}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,148 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'html/pipeline'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe LabelReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:label) { create(:label, project: project) }
|
||||||
|
let(:reference) { "~#{label.id}" }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Label ~123', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Label #{reference}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Label #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Label #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Label #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'label span element' do
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Label #{reference}")
|
||||||
|
expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a style attribute' do
|
||||||
|
doc = filter("Label #{reference}")
|
||||||
|
expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Integer-based references' do
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_issues_url(project.namespace, project, label_name: label.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Label (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid label IDs' do
|
||||||
|
exp = act = "Label ~#{label.id + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'String-based single-word references' do
|
||||||
|
let(:label) { create(:label, name: 'gfm', project: project) }
|
||||||
|
let(:reference) { "~#{label.name}" }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_issues_url(project.namespace, project, label_name: label.name)
|
||||||
|
expect(doc.text).to eq 'See gfm'
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'String-based multi-word references in quotes' do
|
||||||
|
let(:label) { create(:label, name: 'gfm references', project: project) }
|
||||||
|
|
||||||
|
context 'in single quotes' do
|
||||||
|
let(:reference) { "~'#{label.name}'" }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_issues_url(project.namespace, project, label_name: label.name)
|
||||||
|
expect(doc.text).to eq 'See gfm references'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Label (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid label names' do
|
||||||
|
exp = act = "Label ~'#{label.name.reverse}'"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'in double quotes' do
|
||||||
|
let(:reference) { %(~"#{label.name}") }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_issues_url(project.namespace, project, label_name: label.name)
|
||||||
|
expect(doc.text).to eq 'See gfm references'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Label (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid label names' do
|
||||||
|
exp = act = %(Label ~"#{label.name.reverse}")
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,114 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe MergeRequestReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
let(:merge) { create(:merge_request, source_project: project) }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('MergeRequest !123', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'internal reference' do
|
||||||
|
let(:reference) { "!#{merge.iid}" }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_merge_request_url(project.namespace, project, merge)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Merge (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid merge IDs' do
|
||||||
|
exp = act = "Merge !#{merge.iid + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("Merge #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the title attribute' do
|
||||||
|
merge.update_attribute(:title, %{"></a>whatever<a title="})
|
||||||
|
|
||||||
|
doc = filter("Merge #{reference}")
|
||||||
|
expect(doc.text).to eq "Merge #{reference}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Merge #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Merge #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Merge #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cross-project reference' do
|
||||||
|
let(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||||
|
let(:project2) { create(:project, namespace: namespace) }
|
||||||
|
let(:merge) { create(:merge_request, source_project: project2) }
|
||||||
|
let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
|
||||||
|
|
||||||
|
context 'when user can access reference' do
|
||||||
|
before { allow_cross_reference! }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_merge_request_url(project2.namespace,
|
||||||
|
project, merge)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Merge (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid merge IDs on the referenced project' do
|
||||||
|
exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access reference' do
|
||||||
|
before { disallow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references' do
|
||||||
|
exp = act = "See #{reference}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,112 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe SnippetReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:snippet) { create(:project_snippet, project: project) }
|
||||||
|
let(:reference) { "$#{snippet.id}" }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Snippet $123', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'internal reference' do
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.
|
||||||
|
namespace_project_snippet_url(project.namespace, project, snippet)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("Snippet (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid snippet IDs' do
|
||||||
|
exp = act = "Snippet $#{snippet.id + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes a title attribute' do
|
||||||
|
doc = filter("Snippet #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'escapes the title attribute' do
|
||||||
|
snippet.update_attribute(:title, %{"></a>whatever<a title="})
|
||||||
|
|
||||||
|
doc = filter("Snippet #{reference}")
|
||||||
|
expect(doc.text).to eq "Snippet #{reference}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Snippet #{reference}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Snippet #{reference}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Snippet #{reference}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cross-project reference' do
|
||||||
|
let(:namespace) { create(:namespace, name: 'cross-reference') }
|
||||||
|
let(:project2) { create(:empty_project, namespace: namespace) }
|
||||||
|
let(:snippet) { create(:project_snippet, project: project2) }
|
||||||
|
let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
|
||||||
|
|
||||||
|
context 'when user can access reference' do
|
||||||
|
before { allow_cross_reference! }
|
||||||
|
|
||||||
|
it 'links to a valid reference' do
|
||||||
|
doc = filter("See #{reference}")
|
||||||
|
|
||||||
|
expect(doc.css('a').first.attr('href')).
|
||||||
|
to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
doc = filter("See (#{reference}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid snippet IDs on the referenced project' do
|
||||||
|
exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access reference' do
|
||||||
|
before { disallow_cross_reference! }
|
||||||
|
|
||||||
|
it 'ignores valid references' do
|
||||||
|
exp = act = "See #{reference}"
|
||||||
|
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,98 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
module Gitlab::Markdown
|
||||||
|
describe UserReferenceFilter do
|
||||||
|
include ReferenceFilterSpecHelper
|
||||||
|
|
||||||
|
let(:project) { create(:empty_project) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
it 'requires project context' do
|
||||||
|
expect { described_class.call('Example @mention', {}) }.
|
||||||
|
to raise_error(ArgumentError, /:project/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores invalid users' do
|
||||||
|
exp = act = 'Hey @somebody'
|
||||||
|
expect(filter(act).to_html).to eq(exp)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(pre code a style).each do |elem|
|
||||||
|
it "ignores valid references contained inside '#{elem}' element" do
|
||||||
|
exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>"
|
||||||
|
expect(filter(act).to_html).to eq exp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'mentioning a user' do
|
||||||
|
it 'links to a User' do
|
||||||
|
doc = filter("Hey @#{user.username}")
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): This test might be overkill
|
||||||
|
it 'links to a User with a period' do
|
||||||
|
user = create(:user, name: 'alphA.Beta')
|
||||||
|
|
||||||
|
doc = filter("Hey @#{user.username}")
|
||||||
|
expect(doc.css('a').length).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO (rspeicher): This test might be overkill
|
||||||
|
it 'links to a User with an underscore' do
|
||||||
|
user = create(:user, name: 'ping_pong_king')
|
||||||
|
|
||||||
|
doc = filter("Hey @#{user.username}")
|
||||||
|
expect(doc.css('a').length).to eq 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'mentioning a group' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
it 'links to a Group that the current user can read' do
|
||||||
|
group.add_user(user, Gitlab::Access::DEVELOPER)
|
||||||
|
|
||||||
|
doc = filter("Hey @#{group.name}", current_user: user)
|
||||||
|
expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores references to a Group that the current user cannot read' do
|
||||||
|
doc = filter("Hey @#{group.name}", current_user: user)
|
||||||
|
expect(doc.to_html).to eq "Hey @#{group.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'links with adjacent text' do
|
||||||
|
skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.'
|
||||||
|
doc = filter("Mention me (@#{user.username}.)")
|
||||||
|
expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports a special @all mention' do
|
||||||
|
doc = filter("Hey @all")
|
||||||
|
expect(doc.css('a').length).to eq 1
|
||||||
|
expect(doc.css('a').first.attr('href'))
|
||||||
|
.to eq urls.namespace_project_url(project.namespace, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes default classes' do
|
||||||
|
doc = filter("Hey @#{user.username}")
|
||||||
|
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes an optional custom class' do
|
||||||
|
doc = filter("Hey @#{user.username}", reference_class: 'custom')
|
||||||
|
expect(doc.css('a').first.attr('class')).to include 'custom'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'supports an :only_path context' do
|
||||||
|
doc = filter("Hey @#{user.username}", only_path: true)
|
||||||
|
link = doc.css('a').first.attr('href')
|
||||||
|
|
||||||
|
expect(link).not_to match %r(https?://)
|
||||||
|
expect(link).to eq urls.user_path(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -74,7 +74,7 @@ describe Gitlab::ReferenceExtractor do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'handles all possible kinds of references' do
|
it 'handles all possible kinds of references' do
|
||||||
accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym }
|
accessors = described_class::TYPES.map { |t| "#{t}s".to_sym }
|
||||||
expect(subject).to respond_to(*accessors)
|
expect(subject).to respond_to(*accessors)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -106,6 +106,15 @@ describe Gitlab::ReferenceExtractor do
|
||||||
expect(subject.merge_requests).to eq([@m1, @m0])
|
expect(subject.merge_requests).to eq([@m1, @m0])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'accesses valid labels' do
|
||||||
|
@l0 = create(:label, title: 'one', project: project)
|
||||||
|
@l1 = create(:label, title: 'two', project: project)
|
||||||
|
@l2 = create(:label)
|
||||||
|
|
||||||
|
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
|
||||||
|
expect(subject.labels).to eq([@l0, @l1])
|
||||||
|
end
|
||||||
|
|
||||||
it 'accesses valid snippets' do
|
it 'accesses valid snippets' do
|
||||||
@s0 = create(:project_snippet, project: project)
|
@s0 = create(:project_snippet, project: project)
|
||||||
@s1 = create(:project_snippet, project: project)
|
@s1 = create(:project_snippet, project: project)
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Common methods and setup for Gitlab::Markdown reference filter specs
|
||||||
|
#
|
||||||
|
# Must be included into specs manually
|
||||||
|
module ReferenceFilterSpecHelper
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before { set_default_url_options }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allow *_url helpers to work
|
||||||
|
def set_default_url_options
|
||||||
|
Rails.application.routes.default_url_options = {
|
||||||
|
host: 'example.foo'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
|
||||||
|
# module
|
||||||
|
def urls
|
||||||
|
Rails.application.routes.url_helpers
|
||||||
|
end
|
||||||
|
|
||||||
|
# Perform `call` on the described class
|
||||||
|
#
|
||||||
|
# Automatically passes the current `project` value to the context if none is
|
||||||
|
# provided.
|
||||||
|
#
|
||||||
|
# html - String text to pass to the filter's `call` method.
|
||||||
|
# contexts - Hash context for the filter. (default: {project: project})
|
||||||
|
#
|
||||||
|
# Returns the String text returned by the filter's `call` method.
|
||||||
|
def filter(html, contexts = {})
|
||||||
|
contexts.reverse_merge!(project: project)
|
||||||
|
described_class.call(html, contexts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_cross_reference!
|
||||||
|
allow_any_instance_of(described_class).
|
||||||
|
to receive(:user_can_reference_project?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disallow_cross_reference!
|
||||||
|
allow_any_instance_of(described_class).
|
||||||
|
to receive(:user_can_reference_project?).and_return(false)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue