Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
heptapod
heptapod
Commits
5f13cf95e4cb
Commit
96c2b940
authored
Apr 02, 2015
by
Robert Speicher
Browse files
Reference filters
✨
Commit ranges, commits, external issues, issues, labels, merge requests, snippets, users.
parent
eb87bbc254b0
Changes
24
Hide whitespace changes
Inline
Side-by-side
app/helpers/issues_helper.rb
View file @
5f13cf95
...
...
@@ -108,4 +108,6 @@ def issue_to_atom(xml, issue)
xml
.
summary
issue
.
title
end
end
module_function
:url_for_issue
,
:title_for_issue
end
app/helpers/labels_helper.rb
View file @
5f13cf95
...
...
@@ -7,9 +7,13 @@ def render_colored_label(label)
label_color
=
label
.
color
||
Label
::
DEFAULT_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
label
.
name
end
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span
=
%(<span class="label color-label")
+
%( style="background-color: #{label_color}; color: #{text_color}">)
+
label
.
name
+
'</span>'
span
.
html_safe
end
def
suggested_colors
...
...
@@ -42,13 +46,15 @@ def text_color_for_bg(bg_color)
r
,
g
,
b
=
bg_color
.
slice
(
1
,
7
).
scan
(
/.{2}/
).
map
(
&
:hex
)
if
(
r
+
g
+
b
)
>
500
"#
333
"
'#33
333
3'
else
"#
FFF
"
'#FF
FFF
F'
end
end
def
project_labels_options
(
project
)
options_from_collection_for_select
(
project
.
labels
,
'name'
,
'name'
,
params
[
:label_name
])
end
module_function
:render_colored_label
,
:text_color_for_bg
end
lib/gitlab/markdown.rb
View file @
5f13cf95
...
...
@@ -10,11 +10,11 @@ module Gitlab
# Supported reference formats are:
# * @foo for team members
# * #123 for issues
# *
#
JIRA-123 for Jira issues
# * JIRA-123 for Jira issues
# * !123 for merge requests
# * $123 for snippets
# * 1
23456 for
commit
s
# * 1
23456...7890123
for commit ranges (comparisons)
# * 1
c002d for specific
commit
# * 1
c002d...35cfb2
for commit ranges (comparisons)
#
# It also parses Emoji codes to insert images. See
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
...
...
@@ -30,10 +30,6 @@ module Gitlab
# >> gfm(":trollface:")
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
module
Markdown
include
IssuesHelper
attr_reader
:options
,
:html_options
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
...
...
@@ -65,42 +61,13 @@ def gfm_with_options(text, options = {}, project = @project, html_options = {})
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
=
{
asset_root:
Gitlab
.
config
.
gitlab
.
url
,
asset_host:
Gitlab
::
Application
.
config
.
asset_host
,
whitelist:
whitelist
asset_root:
Gitlab
.
config
.
gitlab
.
url
,
asset_host:
Gitlab
::
Application
.
config
.
asset_host
,
whitelist:
sanitization_whitelist
,
reference_class:
html_options
[
:class
],
only_path:
options
[
:reference_only_path
],
project:
project
}
markdown_pipeline
=
HTML
::
Pipeline
::
Gitlab
.
new
(
filters
).
pipeline
...
...
@@ -114,21 +81,6 @@ def gfm_with_options(text, options = {}, project = @project, html_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
]
text
=
parse_tasks
(
text
)
end
...
...
@@ -138,242 +90,48 @@ def gfm_with_options(text, options = {}, project = @project, html_options = {})
private
def
extract_piece
(
text
)
@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
# Custom filters for html-pipeline:
#
# Returns parsed text
def
parse
(
text
,
project
=
@project
)
parse_references
(
text
,
project
)
if
project
text
end
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
|(?<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
]
==
';'
# See https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
def
filters
[
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
,
HTML
::
Pipeline
::
SanitizationFilter
]
end
# Private: Dispatches to a dedicated processing method based on reference
#
# reference - Object reference ("@1234", "!567", etc.)
# identifier - Object identifier (Issue ID, SHA hash, etc.)
# Customize the SanitizationFilter whitelist
#
# Returns string rendered by the processing method
def
reference_link
(
type
,
identifier
,
project
=
@project
,
prefix_text
=
nil
)
send
(
"reference_
#{
type
}
"
,
identifier
,
project
,
prefix_text
)
end
def
reference_user
(
identifier
,
project
=
@project
,
_
=
nil
)
link_options
=
html_options
.
merge
(
class:
"gfm gfm-project_member
#{
html_options
[
:class
]
}
"
)
# - Allow `class` and `id` attributes on all elements
# - Allow `span` elements
# - Remove `rel` attributes from `a` elements
# - Remove `a` nodes with `javascript:` in the `href` attribute
def
sanitization_whitelist
whitelist
=
HTML
::
Pipeline
::
SanitizationFilter
::
WHITELIST
whitelist
[
:attributes
][
:all
].
push
(
'class'
,
'id'
)
whitelist
[
:elements
].
push
(
'span'
)
if
identifier
==
"all"
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
])
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
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
def
reference_merge_request
(
identifier
,
project
=
@project
,
prefix_text
=
nil
)
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
)
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
whitelist
[
:transformers
].
push
(
fix_anchors
)
link_options
=
html_options
.
merge
(
title:
"Issue in
#{
title
}
"
,
class:
"gfm gfm-issue
#{
html_options
[
:class
]
}
"
)
link_to
(
"
#{
prefix_text
}
#
#{
identifier
}
"
,
url
,
link_options
)
whitelist
end
# Turn list items that start with "[ ]" into HTML checkbox inputs.
...
...
lib/gitlab/markdown/commit_range_reference_filter.rb
0 → 100644
View file @
5f13cf95
require
'html/pipeline'
module
Gitlab
module
Markdown
# HTML filter that replaces commit range references with links. References
# within <pre>, <code>, <a>, and <style> elements are ignored.
#
# This filter supports cross-project references.
#
# Context options:
# :project (required) - Current project, ignored when reference is
# cross-project.
# :reference_class - Custom CSS class added to reference links.
# :only_path - Generate path-only links.
#
class
CommitRangeReferenceFilter
<
HTML
::
Pipeline
::
Filter
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
# 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})/
# Don't look for references in text nodes that are children of these
# elements.
IGNORE_PARENTS
=
%w(pre code a style)
.
to_set
def
call
doc
.
search
(
'text()'
).
each
do
|
node
|
content
=
node
.
to_html
next
if
project
.
nil?
next
unless
content
.
match
(
COMMIT_RANGE_PATTERN
)
next
if
has_ancestor?
(
node
,
IGNORE_PARENTS
)
html
=
commit_range_link_filter
(
content
)
next
if
html
==
content
node
.
replace
(
html
)
end
doc
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
=
"gfm gfm-commit_range
#{
context
[
:reference_class
]
}
"
.
strip
project_ref
+=
'@'
if
project_ref
%(<a href="#{url}"
title="#{title}"
class="#{klass}">#{project_ref}#{commit_range}</a>)
else
match
end
end
end
def
validate
needs
:project
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
valid_range?
(
project
,
from_id
,
to_id
)
project
.
valid_repo?
&&
project
.
repository
.
commit
(
from_id
)
&&
project
.
repository
.
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
def
project
context
[
:project
]
end
end
end
end
lib/gitlab/markdown/commit_reference_filter.rb
0 → 100644
View file @
5f13cf95
require
'html/pipeline'
module
Gitlab
module
Markdown
# HTML filter that replaces commit references with links. References within
# <pre>, <code>, <a>, and <style> elements are ignored.
#
# This filter supports cross-project references.
#
# Context options:
# :project (required) - Current project, ignored when reference is
# cross-project.
# :reference_class - Custom CSS class added to reference links.
# :only_path - Generate path-only links.
#
class
CommitReferenceFilter
<
HTML
::
Pipeline
::
Filter
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})/
# Don't look for references in text nodes that are children of these
# elements.
IGNORE_PARENTS
=
%w(pre code a style)
.
to_set
def
call
doc
.
search
(
'text()'
).
each
do
|
node
|
content
=
node
.
to_html
next
if
project
.
nil?
next
unless
content
.
match
(
COMMIT_PATTERN
)
next
if
has_ancestor?
(
node
,
IGNORE_PARENTS
)
html
=
commit_link_filter
(
content
)
next
if
html
==
content
node
.
replace
(
html
)
end
doc
end
def
validate
needs
:project