Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
761bdd32
Commit
761bdd32
authored
Dec 01, 2017
by
Jarka Kadlecova
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support mentioning epics
parent
d15e704e
Changes
40
Show whitespace changes
Inline
Side-by-side
Showing
40 changed files
with
957 additions
and
179 deletions
+957
-179
app/helpers/markup_helper.rb
app/helpers/markup_helper.rb
+2
-0
app/models/concerns/mentionable.rb
app/models/concerns/mentionable.rb
+6
-2
app/models/concerns/mentionable/reference_regexes.rb
app/models/concerns/mentionable/reference_regexes.rb
+1
-0
app/models/epic.rb
app/models/epic.rb
+7
-3
changelogs/unreleased-ee/3853-mention-epics.yml
changelogs/unreleased-ee/3853-mention-epics.yml
+5
-0
doc/user/markdown.md
doc/user/markdown.md
+3
-0
ee/app/models/ee/epic.rb
ee/app/models/ee/epic.rb
+57
-0
ee/lib/ee/banzai/filter/epic_reference_filter.rb
ee/lib/ee/banzai/filter/epic_reference_filter.rb
+56
-0
ee/lib/ee/banzai/reference_parser/epic_parser.rb
ee/lib/ee/banzai/reference_parser/epic_parser.rb
+18
-0
lib/banzai/cross_project_reference.rb
lib/banzai/cross_project_reference.rb
+1
-1
lib/banzai/filter/abstract_reference_filter.rb
lib/banzai/filter/abstract_reference_filter.rb
+60
-38
lib/banzai/filter/epic_reference_filter.rb
lib/banzai/filter/epic_reference_filter.rb
+14
-0
lib/banzai/filter/issuable_reference_filter.rb
lib/banzai/filter/issuable_reference_filter.rb
+31
-0
lib/banzai/filter/issue_reference_filter.rb
lib/banzai/filter/issue_reference_filter.rb
+5
-27
lib/banzai/filter/label_reference_filter.rb
lib/banzai/filter/label_reference_filter.rb
+2
-2
lib/banzai/filter/merge_request_reference_filter.rb
lib/banzai/filter/merge_request_reference_filter.rb
+7
-30
lib/banzai/filter/milestone_reference_filter.rb
lib/banzai/filter/milestone_reference_filter.rb
+1
-1
lib/banzai/issuable_extractor.rb
lib/banzai/issuable_extractor.rb
+2
-2
lib/banzai/pipeline/gfm_pipeline.rb
lib/banzai/pipeline/gfm_pipeline.rb
+1
-0
lib/banzai/pipeline/single_line_pipeline.rb
lib/banzai/pipeline/single_line_pipeline.rb
+2
-1
lib/banzai/reference_parser/epic_parser.rb
lib/banzai/reference_parser/epic_parser.rb
+14
-0
lib/banzai/reference_parser/issuable_parser.rb
lib/banzai/reference_parser/issuable_parser.rb
+25
-0
lib/banzai/reference_parser/issue_parser.rb
lib/banzai/reference_parser/issue_parser.rb
+3
-9
lib/banzai/reference_parser/merge_request_parser.rb
lib/banzai/reference_parser/merge_request_parser.rb
+2
-22
lib/gitlab/reference_extractor.rb
lib/gitlab/reference_extractor.rb
+1
-1
spec/ee/spec/features/epics/referencing_epics_spec.rb
spec/ee/spec/features/epics/referencing_epics_spec.rb
+63
-0
spec/ee/spec/lib/ee/banzai/filter/epic_reference_filter_spec.rb
...e/spec/lib/ee/banzai/filter/epic_reference_filter_spec.rb
+249
-0
spec/ee/spec/lib/ee/banzai/reference_parser/epic_parser_spec.rb
...e/spec/lib/ee/banzai/reference_parser/epic_parser_spec.rb
+83
-0
spec/ee/spec/models/epic_spec.rb
spec/ee/spec/models/epic_spec.rb
+68
-0
spec/features/markdown_spec.rb
spec/features/markdown_spec.rb
+8
-1
spec/fixtures/markdown.md.erb
spec/fixtures/markdown.md.erb
+8
-0
spec/helpers/issuables_helper_spec.rb
spec/helpers/issuables_helper_spec.rb
+1
-1
spec/lib/banzai/cross_project_reference_spec.rb
spec/lib/banzai/cross_project_reference_spec.rb
+4
-4
spec/lib/banzai/filter/abstract_reference_filter_spec.rb
spec/lib/banzai/filter/abstract_reference_filter_spec.rb
+19
-19
spec/lib/banzai/filter/issue_reference_filter_spec.rb
spec/lib/banzai/filter/issue_reference_filter_spec.rb
+43
-4
spec/lib/banzai/reference_parser/issue_parser_spec.rb
spec/lib/banzai/reference_parser/issue_parser_spec.rb
+2
-2
spec/lib/gitlab/reference_extractor_spec.rb
spec/lib/gitlab/reference_extractor_spec.rb
+47
-2
spec/support/markdown_feature.rb
spec/support/markdown_feature.rb
+8
-0
spec/support/matchers/markdown_matchers.rb
spec/support/matchers/markdown_matchers.rb
+9
-0
spec/support/mentionable_shared_examples.rb
spec/support/mentionable_shared_examples.rb
+19
-7
No files found.
app/helpers/markup_helper.rb
View file @
761bdd32
...
...
@@ -86,6 +86,8 @@ module MarkupHelper
return
''
unless
text
.
present?
context
[
:project
]
||=
@project
context
[
:group
]
||=
@group
html
=
markdown_unsafe
(
text
,
context
)
prepare_for_rendering
(
html
,
context
)
end
...
...
app/models/concerns/mentionable.rb
View file @
761bdd32
...
...
@@ -61,7 +61,7 @@ module Mentionable
cache_key:
[
self
,
attr
],
author:
author
,
skip_project_check:
skip_project_check?
)
)
.
merge
(
mentionable_params
)
extractor
.
analyze
(
text
,
options
)
end
...
...
@@ -82,7 +82,7 @@ module Mentionable
return
[]
unless
matches_cross_reference_regex?
refs
=
all_references
(
current_user
)
refs
=
(
refs
.
issues
+
refs
.
merge_requests
+
refs
.
commits
)
refs
=
(
refs
.
issues
+
refs
.
merge_requests
+
refs
.
commits
+
refs
.
epics
)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
...
...
@@ -157,4 +157,8 @@ module Mentionable
def
skip_project_check?
false
end
def
mentionable_params
{}
end
end
app/models/concerns/mentionable/reference_regexes.rb
View file @
761bdd32
...
...
@@ -3,6 +3,7 @@ module Mentionable
def
self
.
reference_pattern
(
link_patterns
,
issue_pattern
)
Regexp
.
union
(
link_patterns
,
issue_pattern
,
Epic
.
reference_pattern
,
Commit
.
reference_pattern
,
MergeRequest
.
reference_pattern
)
end
...
...
app/models/epic.rb
View file @
761bdd32
# Placeholder class for model that is implemented in EE
# It
will reserve (ee#3853)
'&' as a reference prefix, but the table does not exists in CE
# It
reserves
'&' as a reference prefix, but the table does not exists in CE
class
Epic
<
ActiveRecord
::
Base
prepend
EE
::
Epic
# TODO: this will be implemented as part of #3853
def
to_reference
def
self
.
reference_prefix
'&'
end
def
self
.
reference_prefix_escaped
'&'
end
end
changelogs/unreleased-ee/3853-mention-epics.yml
0 → 100644
View file @
761bdd32
---
title
:
Support mentioning epics
merge_request
:
author
:
type
:
added
doc/user/markdown.md
View file @
761bdd32
...
...
@@ -22,6 +22,7 @@ You can use GFM in the following areas:
-
snippets (the snippet must be named with a
`.md`
extension)
-
wiki pages
-
markdown documents inside the repository
-
epics
You can also use other rich text files in GitLab. You might have to install a
dependency to do so. Please see the
[
github-markup gem readme
](
https://github.com/gitlabhq/markup#markups
)
for more information.
...
...
@@ -245,6 +246,7 @@ GFM will recognize the following:
| `#123` | issue |
| `!123` | merge request |
| `$123` | snippet |
| `&123` | epic |
| `~123` | label by ID |
| `~bug` | one-word label by name |
| `~"feature request"` | multi-word label by name |
...
...
@@ -265,6 +267,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project%123` | project milestone |
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `group1/subgroup&123` | epic |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
...
...
ee/app/models/ee/epic.rb
View file @
761bdd32
...
...
@@ -6,6 +6,7 @@ module EE
include
InternalId
include
Issuable
include
Noteable
include
Referable
belongs_to
:assignee
,
class_name:
"User"
belongs_to
:group
...
...
@@ -15,6 +16,46 @@ module EE
validates
:group
,
presence:
true
end
module
ClassMethods
# We support internal references (&epic_id) and cross-references (group.full_path&epic_id)
#
# Escaped versions with `&` will be extracted too
#
# The parent of epic is group instead of project and therefore we have to define new patterns
def
reference_pattern
@reference_pattern
||=
begin
combined_prefix
=
Regexp
.
union
(
Regexp
.
escape
(
reference_prefix
),
Regexp
.
escape
(
reference_prefix_escaped
))
group_regexp
=
%r{
(?<!
\w
)
(?<group>
#{
::
Gitlab
::
PathRegex
::
FULL_NAMESPACE_FORMAT_REGEX
}
)
}x
%r{
(
#{
group_regexp
}
)?
(?:
#{
combined_prefix
}
)(?<epic>
\d
+)
}x
end
end
def
link_reference_pattern
%r{
(?<url>
#{
Regexp
.
escape
(
::
Gitlab
.
config
.
gitlab
.
url
)
}
\/
groups
\/
(?<group>
#{
::
Gitlab
::
PathRegex
::
FULL_NAMESPACE_FORMAT_REGEX
}
)
\/
-
\/
epics
\/
(?<epic>
\d
+)
(?<path>
(
\/
[a-z0-9_=-]+)*
)?
(?<query>
\?
[a-z0-9_=-]+
(&[a-z0-9_=-]+)*
)?
(?<anchor>
\#
[a-z0-9_-]+)?
)
}x
end
end
def
assignees
Array
(
assignee
)
end
...
...
@@ -27,6 +68,18 @@ module EE
false
end
def
to_reference
(
from
=
nil
,
full:
false
)
reference
=
"
#{
self
.
class
.
reference_prefix
}#{
iid
}
"
return
reference
unless
cross_reference?
(
from
)
||
full
"
#{
group
.
full_path
}#{
reference
}
"
end
def
cross_reference?
(
from
)
from
&&
from
!=
group
end
# we don't support project epics for epics yet, planned in the future #4019
def
update_project_counter_caches
end
...
...
@@ -38,5 +91,9 @@ module EE
Ability
.
issues_readable_by_user
(
related_issues
,
current_user
)
end
def
mentionable_params
{
group:
group
}
end
end
end
ee/lib/ee/banzai/filter/epic_reference_filter.rb
0 → 100644
View file @
761bdd32
module
EE
module
Banzai
module
Filter
# HTML filter that replaces epic references with links. References to
# epics that do not exist are ignored.
#
# This filter supports cross-project/group references.
module
EpicReferenceFilter
extend
ActiveSupport
::
Concern
module
ClassMethods
def
references_in
(
text
,
pattern
=
object_class
.
reference_pattern
)
text
.
gsub
(
pattern
)
do
|
match
|
symbol
=
$~
[
object_sym
]
if
object_class
.
reference_valid?
(
symbol
)
yield
match
,
symbol
.
to_i
,
nil
,
$~
[
:group
],
$~
else
match
end
end
end
end
def
url_for_object
(
epic
,
group
)
urls
=
::
Gitlab
::
Routing
.
url_helpers
urls
.
group_epic_url
(
group
,
epic
,
only_path:
context
[
:only_path
])
end
def
data_attributes_for
(
text
,
group
,
object
,
link:
false
)
data_attribute
(
original:
text
,
link:
link
,
group:
group
.
id
,
object_sym
=>
object
.
id
)
end
def
parent_records
(
parent
,
ids
)
parent
.
epics
.
where
(
iid:
ids
.
to_a
)
end
private
def
full_group_path
(
group_ref
)
return
current_parent_path
unless
group_ref
group_ref
end
def
parent_type
:group
end
end
end
end
end
ee/lib/ee/banzai/reference_parser/epic_parser.rb
0 → 100644
View file @
761bdd32
module
EE
module
Banzai
module
ReferenceParser
module
EpicParser
def
records_for_nodes
(
nodes
)
@epics_for_nodes
||=
grouped_objects_for_nodes
(
nodes
,
::
Epic
.
includes
(
:author
,
:group
),
self
.
class
.
data_attribute
)
end
end
end
end
end
lib/banzai/cross_project_reference.rb
View file @
761bdd32
...
...
@@ -11,7 +11,7 @@ module Banzai
# ref - String reference.
#
# Returns a Project, or nil if the reference can't be found
def
p
rojec
t_from_ref
(
ref
)
def
p
aren
t_from_ref
(
ref
)
return
context
[
:project
]
unless
ref
Project
.
find_by_full_path
(
ref
)
...
...
lib/banzai/filter/abstract_reference_filter.rb
View file @
761bdd32
...
...
@@ -82,9 +82,9 @@ module Banzai
end
end
def
project_
from_ref_cached
(
ref
)
cached_call
(
:banzai_project_refs
,
ref
)
do
p
rojec
t_from_ref
(
ref
)
def
from_ref_cached
(
ref
)
cached_call
(
"banzai_
#{
parent_type
}
_refs"
.
to_sym
,
ref
)
do
p
aren
t_from_ref
(
ref
)
end
end
...
...
@@ -153,15 +153,20 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def
object_link_filter
(
text
,
pattern
,
link_content:
nil
,
link_reference:
false
)
references_in
(
text
,
pattern
)
do
|
match
,
id
,
project_ref
,
namespace_ref
,
matches
|
project_path
=
full_project_path
(
namespace_ref
,
project_ref
)
project
=
project_from_ref_cached
(
project_path
)
parent_path
=
if
parent_type
==
:group
full_group_path
(
namespace_ref
)
else
full_project_path
(
namespace_ref
,
project_ref
)
end
if
project
parent
=
from_ref_cached
(
parent_path
)
if
parent
object
=
if
link_reference
find_object_from_link_cached
(
p
rojec
t
,
id
)
find_object_from_link_cached
(
p
aren
t
,
id
)
else
find_object_cached
(
p
rojec
t
,
id
)
find_object_cached
(
p
aren
t
,
id
)
end
end
...
...
@@ -169,13 +174,13 @@ module Banzai
title
=
object_link_title
(
object
)
klass
=
reference_class
(
object_sym
)
data
=
data_attributes_for
(
link_content
||
match
,
p
rojec
t
,
object
,
link:
!!
link_content
)
data
=
data_attributes_for
(
link_content
||
match
,
p
aren
t
,
object
,
link:
!!
link_content
)
url
=
if
matches
.
names
.
include?
(
"url"
)
&&
matches
[
:url
]
matches
[
:url
]
else
url_for_object_cached
(
object
,
p
rojec
t
)
url_for_object_cached
(
object
,
p
aren
t
)
end
content
=
link_content
||
object_link_text
(
object
,
matches
)
...
...
@@ -224,17 +229,24 @@ module Banzai
# Returns a Hash containing all object references (e.g. issue IDs) per the
# project they belong to.
def
references_per_project
@references_per_project
||=
begin
def
references_per_parent
@references_per
||=
{}
@references_per
[
parent_type
]
||=
begin
refs
=
Hash
.
new
{
|
hash
,
key
|
hash
[
key
]
=
Set
.
new
}
regex
=
Regexp
.
union
(
object_class
.
reference_pattern
,
object_class
.
link_reference_pattern
)
nodes
.
each
do
|
node
|
node
.
to_html
.
scan
(
regex
)
do
project_path
=
full_project_path
(
$~
[
:namespace
],
$~
[
:project
])
path
=
if
parent_type
==
:project
full_project_path
(
$~
[
:namespace
],
$~
[
:project
])
else
full_group_path
(
$~
[
:group
])
end
symbol
=
$~
[
object_sym
]
refs
[
p
roject_p
ath
]
<<
symbol
if
object_class
.
reference_valid?
(
symbol
)
refs
[
path
]
<<
symbol
if
object_class
.
reference_valid?
(
symbol
)
end
end
...
...
@@ -244,35 +256,41 @@ module Banzai
# Returns a Hash containing referenced projects grouped per their full
# path.
def
projects_per_reference
@projects_per_reference
||=
begin
def
parent_per_reference
@per_reference
||=
{}
@per_reference
[
parent_type
]
||=
begin
refs
=
Set
.
new
references_per_p
roject
.
each
do
|
project_
ref
,
_
|
refs
<<
project_
ref
references_per_p
arent
.
each
do
|
ref
,
_
|
refs
<<
ref
end
find_
projects_
for_paths
(
refs
.
to_a
).
index_by
(
&
:full_path
)
find_for_paths
(
refs
.
to_a
).
index_by
(
&
:full_path
)
end
end
def
projects_relation_for_paths
(
paths
)
Project
.
where_full_path_in
(
paths
).
includes
(
:namespace
)
def
relation_for_paths
(
paths
)
klass
=
parent_type
.
to_s
.
camelize
.
constantize
result
=
klass
.
where_full_path_in
(
paths
)
return
result
if
parent_type
==
:group
result
.
includes
(
:namespace
)
if
parent_type
==
:project
end
# Returns projects for the given paths.
def
find_
projects_
for_paths
(
paths
)
def
find_for_paths
(
paths
)
if
RequestStore
.
active?
cache
=
project_
refs_cache
cache
=
refs_cache
to_query
=
paths
-
cache
.
keys
unless
to_query
.
empty?
projects
=
projects_
relation_for_paths
(
to_query
)
records
=
relation_for_paths
(
to_query
)
found
=
[]
projects
.
each
do
|
project
|
ref
=
project
.
full_path
get_or_set_cache
(
cache
,
ref
)
{
project
}
records
.
each
do
|
record
|
ref
=
record
.
full_path
get_or_set_cache
(
cache
,
ref
)
{
record
}
found
<<
ref
end
...
...
@@ -284,33 +302,37 @@ module Banzai
cache
.
slice
(
*
paths
).
values
.
compact
else
projects_
relation_for_paths
(
paths
)
relation_for_paths
(
paths
)
end
end
def
current_project_path
return
unless
project
@current_project_path
||=
project
.
full_path
def
current_parent_path
@current_parent_path
||=
parent
&
.
full_path
end
def
current_project_namespace_path
return
unless
project
@current_project_namespace_path
||=
project
.
namespace
.
full_path
@current_project_namespace_path
||=
project
&
.
namespace
&
.
full_path
end
private
def
full_project_path
(
namespace
,
project_ref
)
return
current_p
rojec
t_path
unless
project_ref
return
current_p
aren
t_path
unless
project_ref
namespace_ref
=
namespace
||
current_project_namespace_path
"
#{
namespace_ref
}
/
#{
project_ref
}
"
end
def
project_refs_cache
RequestStore
[
:banzai_project_refs
]
||=
{}
def
refs_cache
RequestStore
[
"banzai_
#{
parent_type
}
_refs"
.
to_sym
]
||=
{}
end
def
parent_type
:project
end
def
parent
parent_type
==
:project
?
project
:
group
end
end
end
...
...
lib/banzai/filter/epic_reference_filter.rb
0 → 100644
View file @
761bdd32
module
Banzai
module
Filter
# The actual filter is implemented in the EE mixin
class
EpicReferenceFilter
<
IssuableReferenceFilter
prepend
EE
::
Banzai
::
Filter
::
EpicReferenceFilter
self
.
reference_type
=
:epic
def
self
.
object_class
Epic
end
end
end
end
lib/banzai/filter/issuable_reference_filter.rb
0 → 100644
View file @
761bdd32
module
Banzai
module
Filter
class
IssuableReferenceFilter
<
AbstractReferenceFilter
def
records_per_parent
@records_per_project
||=
{}
@records_per_project
[
object_class
.
to_s
.
underscore
]
||=
begin
hash
=
Hash
.
new
{
|
h
,
k
|
h
[
k
]
=
{}
}
parent_per_reference
.
each
do
|
path
,
parent
|
record_ids
=
references_per_parent
[
path
]
parent_records
(
parent
,
record_ids
).
each
do
|
record
|
hash
[
parent
][
record
.
iid
.
to_i
]
=
record
end
end
hash
end
end
def
find_object
(
parent
,
iid
)
records_per_parent
[
parent
][
iid
]
end
def
parent_from_ref
(
ref
)
parent_per_reference
[
ref
||
current_parent_path
]
end
end
end
end
lib/banzai/filter/issue_reference_filter.rb
View file @
761bdd32
...
...
@@ -8,46 +8,24 @@ module Banzai
# When external issues tracker like Jira is activated we should not
# use issue reference pattern, but we should still be able
# to reference issues from other GitLab projects.
class
IssueReferenceFilter
<
Abstract
ReferenceFilter
class
IssueReferenceFilter
<
Issuable
ReferenceFilter
self
.
reference_type
=
:issue
def
self
.
object_class
Issue
end
def
find_object
(
project
,
iid
)
issues_per_project
[
project
][
iid
]
end
def
url_for_object
(
issue
,
project
)
IssuesHelper
.
url_for_issue
(
issue
.
iid
,
project
,
only_path:
context
[
:only_path
],
internal:
true
)
end
def
project_from_ref
(
ref
)
projects_per_reference
[
ref
||
current_project_path
]
end
# Returns a Hash containing the issues per Project instance.
def
issues_per_project
@issues_per_project
||=
begin
hash
=
Hash
.
new
{
|
h
,
k
|
h
[
k
]
=
{}
}
projects_per_reference
.
each
do
|
path
,
project
|
issue_ids
=
references_per_project
[
path
]
issues
=
project
.
issues
.
where
(
iid:
issue_ids
.
to_a
)
issues
.
each
do
|
issue
|
hash
[
project
][
issue
.
iid
.
to_i
]
=
issue
end
end
hash
end
end
def
projects_relation_for_paths
(
paths
)
super
(
paths
).
includes
(
:gitlab_issue_tracker_service
)
end
def
parent_records
(
parent
,
ids
)
parent
.
issues
.
where
(
iid:
ids
.
to_a
)
end
end
end
end
lib/banzai/filter/label_reference_filter.rb
View file @
761bdd32
...
...
@@ -33,7 +33,7 @@ module Banzai
end
def
find_label
(
project_ref
,
label_id
,
label_name
)
project
=
p
rojec
t_from_ref
(
project_ref
)
project
=
p
aren
t_from_ref
(
project_ref
)
return
unless
project
label_params
=
label_params
(
label_id
,
label_name
)
...
...
@@ -66,7 +66,7 @@ module Banzai
def
object_link_text
(
object
,
matches
)
project_path
=
full_project_path
(
matches
[
:namespace
],
matches
[
:project
])
project_from_ref
=
project_
from_ref_cached
(
project_path
)
project_from_ref
=
from_ref_cached
(
project_path
)
reference
=
project_from_ref
.
to_human_reference
(
project
)
label_suffix
=
" <i>in
#{
reference
}
</i>"
if
reference
.
present?
...
...
lib/banzai/filter/merge_request_reference_filter.rb
View file @
761bdd32
...
...
@@ -4,48 +4,19 @@ module Banzai
# to merge requests that do not exist are ignored.
#
# This filter supports cross-project references.
class
MergeRequestReferenceFilter
<
Abstract
ReferenceFilter
class
MergeRequestReferenceFilter
<
Issuable
ReferenceFilter
self
.
reference_type
=
:merge_request
def
self
.
object_class
MergeRequest
end
def
find_object
(
project
,
iid
)
merge_requests_per_project
[
project
][
iid
]
end
def
url_for_object
(
mr
,
project
)
h
=
Gitlab
::
Routing
.
url_helpers
h
.
project_merge_request_url
(
project
,
mr
,
only_path:
context
[
:only_path
])
end
def
project_from_ref
(
ref
)
projects_per_reference
[
ref
||
current_project_path
]
end
# Returns a Hash containing the merge_requests per Project instance.
def
merge_requests_per_project
@merge_requests_per_project
||=
begin
hash
=
Hash
.
new
{
|
h
,
k
|
h
[
k
]
=
{}
}
projects_per_reference
.
each
do
|
path
,
project
|
merge_request_ids
=
references_per_project
[
path
]
merge_requests
=
project
.
merge_requests
.
where
(
iid:
merge_request_ids
.
to_a
)
.
includes
(
target_project: :namespace
)
merge_requests
.
each
do
|
merge_request
|
hash
[
project
][
merge_request
.
iid
.
to_i
]
=
merge_request
end
end
hash
end
end
def
object_link_text_extras
(
object
,
matches
)
extras
=
super
...
...
@@ -61,6 +32,12 @@ module Banzai
extras
end
def
parent_records
(
parent
,
ids
)
parent
.
merge_requests
.
where
(
iid:
ids
.
to_a
)
.
includes
(
target_project: :namespace
)
end
end
end
end
lib/banzai/filter/milestone_reference_filter.rb
View file @
761bdd32
...
...
@@ -38,7 +38,7 @@ module Banzai
def
find_milestone
(
project_ref
,
namespace_ref
,
milestone_id
,
milestone_name
)
project_path
=
full_project_path
(
namespace_ref
,
project_ref
)
project
=
p
rojec
t_from_ref
(
project_path
)
project
=
p
aren
t_from_ref
(
project_path
)
return
unless
project
...
...
lib/banzai/issuable_extractor.rb
View file @
761bdd32
...
...
@@ -28,8 +28,8 @@ module Banzai
issue_parser
=
Banzai
::
ReferenceParser
::
IssueParser
.
new
(
project
,
user
)
merge_request_parser
=
Banzai
::
ReferenceParser
::
MergeRequestParser
.
new
(
project
,
user
)
issuables_for_nodes
=
issue_parser
.
issue
s_for_nodes
(
nodes
).
merge
(
merge_request_parser
.
merge_request
s_for_nodes
(
nodes
)
issuables_for_nodes
=
issue_parser
.
record
s_for_nodes
(
nodes
).
merge
(
merge_request_parser
.
record
s_for_nodes
(
nodes
)
)
# The project for the issue/MR might be pending for deletion!
...
...
lib/banzai/pipeline/gfm_pipeline.rb
View file @
761bdd32
...
...
@@ -24,6 +24,7 @@ module Banzai
Filter
::
AutolinkFilter
,
Filter
::
ExternalLinkFilter
,
Filter
::
EpicReferenceFilter
,
Filter
::
UserReferenceFilter
,
Filter
::
IssueReferenceFilter
,
Filter
::
ExternalIssueReferenceFilter
,
...
...
lib/banzai/pipeline/single_line_pipeline.rb
View file @
761bdd32
...
...
@@ -10,13 +10,14 @@ module Banzai
Filter
::
AutolinkFilter
,
Filter
::
ExternalLinkFilter
,
Filter
::
EpicReferenceFilter
,
Filter
::
UserReferenceFilter
,
Filter
::
IssueReferenceFilter
,
Filter
::
ExternalIssueReferenceFilter
,
Filter
::
MergeRequestReferenceFilter
,
Filter
::
SnippetReferenceFilter
,
Filter
::
CommitRangeReferenceFilter
,
Filter
::
CommitReferenceFilter
,
Filter
::
CommitReferenceFilter
]
end
end
...
...
lib/banzai/reference_parser/epic_parser.rb
0 → 100644
View file @
761bdd32
module
Banzai
module
ReferenceParser
# The actual parser is implemented in the EE mixin
class
EpicParser
<
IssuableParser
prepend
EE
::
Banzai
::
ReferenceParser
::
EpicParser
self
.
reference_type
=
:epic
def
records_for_nodes
(
_nodes
)
{}
end
end
end
end
lib/banzai/reference_parser/issuable_parser.rb
0 → 100644
View file @
761bdd32
module
Banzai
module
ReferenceParser
class
IssuableParser
<
BaseParser
def
nodes_visible_to_user
(
user
,
nodes
)
records
=
records_for_nodes
(
nodes
)
nodes
.
select
do
|
node
|
issuable
=
records
[
node
]
issuable
&&
can_read_reference?
(
user
,
issuable
)
end
end
def
referenced_by
(
nodes
)
records
=
records_for_nodes
(
nodes
)
nodes
.
map
{
|
node
|
records
[
node
]
}.
compact
.
uniq
end
def
can_read_reference?
(
user
,
issuable
)
can?
(
user
,
"read_
#{
issuable
.
class
.
to_s
.
underscore
}
"
.
to_sym
,
issuable
)
end
end
end
end
lib/banzai/reference_parser/issue_parser.rb
View file @
761bdd32
module
Banzai
module
ReferenceParser
class
IssueParser
<
Bas
eParser
class
IssueParser
<
Issuabl
eParser
self
.
reference_type
=
:issue
def
nodes_visible_to_user
(
user
,
nodes
)
issues
=
issue
s_for_nodes
(
nodes
)
issues
=
record
s_for_nodes
(
nodes
)
readable_issues
=
Ability
.
issues_readable_by_user
(
issues
.
values
,
user
).
to_set
...
...
@@ -13,13 +13,7 @@ module Banzai
end
end
def
referenced_by
(
nodes
)
issues
=
issues_for_nodes
(
nodes
)
nodes
.
map
{
|
node
|
issues
[
node
]
}.
compact
.
uniq
end
def
issues_for_nodes
(
nodes
)
def
records_for_nodes
(
nodes
)
@issues_for_nodes
||=
grouped_objects_for_nodes
(
nodes
,
Issue
.
all
.
includes
(
...
...
lib/banzai/reference_parser/merge_request_parser.rb
View file @
761bdd32
module
Banzai
module
ReferenceParser
class
MergeRequestParser
<
Bas
eParser
class
MergeRequestParser
<
Issuabl
eParser
self
.
reference_type
=
:merge_request
def
nodes_visible_to_user
(
user
,
nodes
)
merge_requests
=
merge_requests_for_nodes
(
nodes
)
nodes
.
select
do
|
node
|
merge_request
=
merge_requests
[
node
]
merge_request
&&
can?
(
user
,
:read_merge_request
,
merge_request
.
project
)
end
end
def
referenced_by
(
nodes
)
merge_requests
=
merge_requests_for_nodes
(
nodes
)
nodes
.
map
{
|
node
|
merge_requests
[
node
]
}.
compact
.
uniq
end
def
merge_requests_for_nodes
(
nodes
)
def
records_for_nodes
(
nodes
)
@merge_requests_for_nodes
||=
grouped_objects_for_nodes
(
nodes
,
MergeRequest
.
includes
(
...
...
@@ -40,10 +24,6 @@ module Banzai
self
.
class
.
data_attribute
)
end
def
can_read_reference?
(
user
,
ref_project
,
node
)
can?
(
user
,
:read_merge_request
,
ref_project
)
end
end
end
end
lib/gitlab/reference_extractor.rb
View file @
761bdd32
module
Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class
ReferenceExtractor
<
Banzai
::
ReferenceExtractor
REFERABLES
=
%i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user)
.
freeze
REFERABLES
=
%i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user
epic
)
.
freeze
attr_accessor
:project
,
:current_user
,
:author
def
initialize
(
project
,
current_user
=
nil
)
...
...
spec/ee/spec/features/epics/referencing_epics_spec.rb
0 → 100644
View file @
761bdd32
require
'spec_helper'
describe
'Referencing Epics'
,
:js
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:project
)
{
create
(
:project
,
:public
)
}
let
(
:reference
)
{
epic
.
to_reference
(
full:
true
)
}
context
'reference on an issue'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
,
description:
"Check
#{
reference
}
"
)
}
before
do
stub_licensed_features
(
epics:
true
)
sign_in
(
user
)
end
context
'when non group member displays the issue'
do
context
'when referenced epic is in a public group'
do
it
'displays link to the reference'
do
visit
project_issue_path
(
project
,
issue
)
page
.
within
(
'.issuable-details .description'
)
do
expect
(
page
).
to
have_link
(
reference
,
href:
group_epic_path
(
group
,
epic
))
end
end
end
context
'when referenced epic is in a private group'
do
before
do
group
.
update_attribute
(
:visibility_level
,
Gitlab
::
VisibilityLevel
::
PRIVATE
)
end
it
'does not display link to the reference'
do
visit
project_issue_path
(
project
,
issue
)
page
.
within
(
'.issuable-details .description'
)
do
expect
(
page
).
not_to
have_link
end
end
end
end
context
'when a group member displays the issue'
do
context
'when referenced epic is in a private group'
do
before
do
group
.
add_developer
(
user
)
group
.
update_attribute
(
:visibility_level
,
Gitlab
::
VisibilityLevel
::
PRIVATE
)
end
it
'displays link to the reference'
do
visit
project_issue_path
(
project
,
issue
)
page
.
within
(
'.issuable-details .description'
)
do
expect
(
page
).
to
have_link
(
reference
,
href:
group_epic_path
(
group
,
epic
))
end
end
end
end
end
end
spec/ee/spec/lib/ee/banzai/filter/epic_reference_filter_spec.rb
0 → 100644
View file @
761bdd32
require
'spec_helper'
describe
Banzai
::
Filter
::
EpicReferenceFilter
do
include
FilterSpecHelper
let
(
:urls
)
{
Gitlab
::
Routing
.
url_helpers
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:another_group
)
{
create
(
:group
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:full_ref_text
)
{
"Check
#{
epic
.
group
.
full_path
}
&
#{
epic
.
iid
}
"
}
def
doc
(
reference
=
nil
)
reference
||=
"Check &
#{
epic
.
iid
}
"
context
=
{
project:
nil
,
group:
group
}
reference_filter
(
reference
,
context
)
end
context
'internal reference'
do
let
(
:reference
)
{
"&
#{
epic
.
iid
}
"
}
it
'links to a valid reference'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
group
,
epic
))
end
it
'links with adjacent text'
do
expect
(
doc
.
text
).
to
eq
(
"Check
#{
reference
}
"
)
end
it
'includes a title attribute'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'title'
)).
to
eq
(
epic
.
title
)
end
it
'escapes the title attribute'
do
epic
.
update_attribute
(
:title
,
%{"></a>whatever<a title="}
)
expect
(
doc
.
text
).
to
eq
(
"Check
#{
reference
}
"
)
end
it
'includes default classes'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
it
'includes a data-group attribute'
do
link
=
doc
.
css
(
'a'
).
first
expect
(
link
).
to
have_attribute
(
'data-group'
)
expect
(
link
.
attr
(
'data-group'
)).
to
eq
(
group
.
id
.
to_s
)
end
it
'includes a data-epic attribute'
do
link
=
doc
.
css
(
'a'
).
first
expect
(
link
).
to
have_attribute
(
'data-epic'
)
expect
(
link
.
attr
(
'data-epic'
)).
to
eq
(
epic
.
id
.
to_s
)
end
it
'includes a data-original attribute'
do
link
=
doc
.
css
(
'a'
).
first
expect
(
link
).
to
have_attribute
(
'data-original'
)
expect
(
link
.
attr
(
'data-original'
)).
to
eq
(
reference
)
end
it
'ignores invalid epic IDs'
do
text
=
"Check &9999"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
it
'does not process links containing epic numbers followed by text'
do
href
=
"
#{
reference
}
st"
link
=
doc
(
"<a href='
#{
href
}
'></a>"
).
css
(
'a'
).
first
.
attr
(
'href'
)
expect
(
link
).
to
eq
(
href
)
end
end
context
'internal escaped reference'
do
let
(
:reference
)
{
"&
#{
epic
.
iid
}
"
}
it
'links to a valid reference'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
group
,
epic
))
end
it
'includes a title attribute'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'title'
)).
to
eq
(
epic
.
title
)
end
it
'includes default classes'
do
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
it
'ignores invalid epic IDs'
do
text
=
"Check &9999"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
end
context
'cross-reference'
do
before
do
epic
.
update_attribute
(
:group_id
,
another_group
.
id
)
end
it
'ignores a shorthand reference from another group'
do
text
=
"Check &
#{
epic
.
iid
}
"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
it
'links to a valid reference for full reference'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
another_group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
text
).
to
eq
(
"
#{
epic
.
group
.
full_path
}
&
#{
epic
.
iid
}
"
)
end
it
'includes default classes'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
context
'escaped cross-reference'
do
before
do
epic
.
update_attribute
(
:group_id
,
another_group
.
id
)
end
it
'ignores a shorthand reference from another group'
do
text
=
"Check &
#{
epic
.
iid
}
"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
it
'links to a valid reference for full reference'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
another_group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
text
).
to
eq
(
"
#{
epic
.
group
.
full_path
}
&
#{
epic
.
iid
}
"
)
end
it
'includes default classes'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
context
'subgroup cross-reference'
do
before
do
subgroup
=
create
(
:group
,
parent:
another_group
)
epic
.
update_attribute
(
:group_id
,
subgroup
.
id
)
end
it
'ignores a shorthand reference from another group'
do
text
=
"Check &
#{
epic
.
iid
}
"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
it
'ignores reference with incomplete group path'
do
text
=
"Check @
#{
epic
.
group
.
path
}
&
#{
epic
.
iid
}
"
expect
(
doc
(
text
).
to_s
).
to
eq
(
ERB
::
Util
.
html_escape_once
(
text
))
end
it
'links to a valid reference for full reference'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
epic
.
group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
text
).
to
eq
(
"
#{
epic
.
group
.
full_path
}
&
#{
epic
.
iid
}
"
)
end
it
'includes default classes'
do
expect
(
doc
(
full_ref_text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
context
'url reference'
do
let
(
:link
)
{
urls
.
group_epic_url
(
epic
.
group
,
epic
)
}
let
(
:text
)
{
"Check
#{
link
}
"
}
before
do
epic
.
update_attribute
(
:group_id
,
another_group
.
id
)
end
it
'links to a valid reference'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
another_group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
text
).
to
eq
(
epic
.
to_reference
(
group
))
end
it
'includes default classes'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
context
'full cross-refererence in a link href'
do
let
(
:link
)
{
"
#{
another_group
.
path
}
&
#{
epic
.
iid
}
"
}
let
(
:text
)
do
ref
=
%{<a href="#{link}">Reference</a>}
"Check
#{
ref
}
"
end
before
do
epic
.
update_attribute
(
:group_id
,
another_group
.
id
)
end
it
'links to a valid reference for link href'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
another_group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
text
).
to
eq
(
'Reference'
)
end
it
'includes default classes'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
context
'url in a link href'
do
let
(
:link
)
{
urls
.
group_epic_url
(
epic
.
group
,
epic
)
}
let
(
:text
)
do
ref
=
%{<a href="#{link}">Reference</a>}
"Check
#{
ref
}
"
end
before
do
epic
.
update_attribute
(
:group_id
,
another_group
.
id
)
end
it
'links to a valid reference for link href'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'href'
)).
to
eq
(
urls
.
group_epic_url
(
another_group
,
epic
))
end
it
'link has valid text'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
text
).
to
eq
(
'Reference'
)
end
it
'includes default classes'
do
expect
(
doc
(
text
).
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
(
'gfm gfm-epic has-tooltip'
)
end
end
end
spec/ee/spec/lib/ee/banzai/reference_parser/epic_parser_spec.rb
0 → 100644
View file @
761bdd32
require
'spec_helper'
describe
Banzai
::
ReferenceParser
::
EpicParser
do
include
ReferenceParserHelpers
def
link
(
epic_id
)
link
=
empty_html_link
link
[
'data-epic'
]
=
epic_id
.
to_s
link
end
let
(
:user
)
{
create
(
:user
)
}
let
(
:public_group
)
{
create
(
:group
,
:public
)
}
let
(
:private_group1
)
{
create
(
:group
,
:private
)
}
let
(
:private_group2
)
{
create
(
:group
,
:private
)
}
let
(
:public_epic
)
{
create
(
:epic
,
group:
public_group
)
}
let
(
:private_epic1
)
{
create
(
:epic
,
group:
private_group1
)
}
let
(
:private_epic2
)
{
create
(
:epic
,
group:
private_group2
)
}
let
(
:nodes
)
do
[
link
(
public_epic
.
id
),
link
(
private_epic1
.
id
),
link
(
private_epic2
.
id
)]
end
subject
{
described_class
.
new
(
nil
,
user
)
}
describe
'#nodes_visible_to_user'
do
before
do
private_group1
.
add_developer
(
user
)
end
context
'when the epics feature is enabled'
do
before
do
stub_licensed_features
(
epics:
true
)
end
it
'returns the nodes the user can read for valid epic nodes'
do
expected_result
=
[
nodes
[
0
],
nodes
[
1
]]
expect
(
subject
.
nodes_visible_to_user
(
user
,
nodes
)).
to
match_array
(
expected_result
)
end
it
'returns an empty array for nodes without required data-attributes'
do
expect
(
subject
.
nodes_visible_to_user
(
user
,
[
empty_html_link
])).
to
be_empty
end
end
context
'when the epics feature is disabled'
do
it
'returns an empty array'
do
expect
(
subject
.
nodes_visible_to_user
(
user
,
nodes
)).
to
be_empty
end
end
end
describe
'#referenced_by'
do
context
'when using an existing epics IDs'
do
it
'returns an Array of epics'
do
expected_result
=
[
public_epic
,
private_epic1
,
private_epic2
]
expect
(
subject
.
referenced_by
(
nodes
)).
to
match_array
(
expected_result
)
end
it
'returns an empty Array for empty list of nodes'
do
expect
(
subject
.
referenced_by
([])).
to
be_empty
end
end
context
'when epic with given ID does not exist'
do
it
'returns an empty Array'
do
expect
(
subject
.
referenced_by
([
link
(
9999
)])).
to
be_empty
end
end
end
describe
'#records_for_nodes'
do
it
'returns a Hash containing the epics for a list of nodes'
do
expected_hash
=
{
nodes
[
0
]
=>
public_epic
,
nodes
[
1
]
=>
private_epic1
,
nodes
[
2
]
=>
private_epic2
}
expect
(
subject
.
records_for_nodes
(
nodes
)).
to
eq
(
expected_hash
)
end
end
end
spec/ee/spec/models/epic_spec.rb
View file @
761bdd32
...
...
@@ -58,4 +58,72 @@ describe Epic do
expect
(
result
.
map
(
&
:epic_issue_id
)).
to
match_array
([
epic_issues
.
first
.
id
])
end
end
describe
'#to_reference'
do
let
(
:group
)
{
create
(
:group
,
path:
'group-a'
)
}
let
(
:epic
)
{
create
(
:epic
,
iid:
1
,
group:
group
)
}
context
'when nil argument'
do
it
'returns epic id'
do
expect
(
epic
.
to_reference
).
to
eq
(
'&1'
)
end
end
context
'when group argument equals epic group'
do
it
'returns epic id'
do
expect
(
epic
.
to_reference
(
epic
.
group
)).
to
eq
(
'&1'
)
end
end
context
'when group argument differs from epic group'
do
it
'returns complete path to the epic'
do
expect
(
epic
.
to_reference
(
create
(
:group
))).
to
eq
(
'group-a&1'
)
end
end
context
'when full is true'
do
it
'returns complete path to the epic'
do
expect
(
epic
.
to_reference
(
full:
true
)).
to
eq
(
'group-a&1'
)
expect
(
epic
.
to_reference
(
epic
.
group
,
full:
true
)).
to
eq
(
'group-a&1'
)
expect
(
epic
.
to_reference
(
group
,
full:
true
)).
to
eq
(
'group-a&1'
)
end
end
end
context
'mentioning other objects'
do
let
(
:group
)
{
create
(
:group
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:project
)
{
create
(
:project
,
:repository
,
:public
)
}
let
(
:mentioned_issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:mentioned_mr
)
{
create
(
:merge_request
,
source_project:
project
)
}
let
(
:mentioned_commit
)
{
project
.
commit
(
"HEAD~1"
)
}
let
(
:backref_text
)
{
"epic
#{
epic
.
to_reference
}
"
}
let
(
:ref_text
)
do
<<-
MSG
.
strip_heredoc
These are simple references:
Issue:
#{
mentioned_issue
.
to_reference
(
group
)
}
Merge Request:
#{
mentioned_mr
.
to_reference
(
group
)
}
Commit:
#{
mentioned_commit
.
to_reference
(
group
)
}
This is a self-reference and should not be mentioned at all:
Self:
#{
backref_text
}
MSG
end
before
do
epic
.
description
=
ref_text
epic
.
save
end
it
'creates new system notes for cross references'
do
[
mentioned_issue
,
mentioned_mr
,
mentioned_commit
].
each
do
|
newref
|
expect
(
SystemNoteService
).
to
receive
(
:cross_reference
)
.
with
(
newref
,
epic
,
epic
.
author
)
end
epic
.
create_new_cross_references!
(
epic
.
author
)
end
end
end
spec/features/markdown_spec.rb
View file @
761bdd32
...
...
@@ -42,6 +42,10 @@ describe 'GitLab Markdown' do
@doc
||=
Nokogiri
::
HTML
::
DocumentFragment
.
parse
(
html
)
end
before
do
stub_licensed_features
(
epics:
true
)
end
# Shared behavior that all pipelines should exhibit
shared_examples
'all pipelines'
do
describe
'Redcarpet extensions'
do
...
...
@@ -207,8 +211,9 @@ describe 'GitLab Markdown' do
before
do
@feat
=
MarkdownFeature
.
new
# `markdown` helper expects a `@project` variable
# `markdown` helper expects a `@project`
and `@group`
variable
@project
=
@feat
.
project
@group
=
@feat
.
group
end
context
'default pipeline'
do
...
...
@@ -244,6 +249,7 @@ describe 'GitLab Markdown' do
expect
(
doc
).
to
reference_commits
expect
(
doc
).
to
reference_labels
expect
(
doc
).
to
reference_milestones
expect
(
doc
).
to
reference_epics
end
end
...
...
@@ -301,6 +307,7 @@ describe 'GitLab Markdown' do
expect
(
doc
).
to
reference_commits
expect
(
doc
).
to
reference_labels
expect
(
doc
).
to
reference_milestones
expect
(
doc
).
to
reference_epics
end
end
...
...
spec/fixtures/markdown.md.erb
View file @
761bdd32
...
...
@@ -233,6 +233,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Group milestone by name in quotes:
<%=
group_milestone
.
to_reference
(
format: :name
)
%>
- Group milestone by URL is ignore:
<%=
urls
.
milestone_url
(
group_milestone
)
%>
#### EpicReferenceFilter
- Epic by ID:
<%=
epic
.
to_reference
%>
- Epic in another group:
<%=
epic_other_group
.
to_reference
(
group
)
%>
- Epic by url:
<%=
urls
.
group_epic_url
(
epic
.
group
,
epic
)
%>
- Link to epic by reference: [Epic](
<%=
epic
.
to_reference
(
group
)
%>
)
- Link to epic by URL: [Epic](
<%=
urls
.
group_epic_url
(
epic
.
group
,
epic
)
%>
)
### Task Lists
- [ ] Incomplete task 1
...
...
spec/helpers/issuables_helper_spec.rb
View file @
761bdd32
...
...
@@ -204,7 +204,7 @@ describe IssuablesHelper do
'canUpdate'
=>
true
,
'canDestroy'
=>
true
,
'canAdmin'
=>
true
,
'issuableRef'
=>
nil
,
'issuableRef'
=>
"&
#{
epic
.
iid
}
"
,
'markdownPreviewPath'
=>
"/groups/
#{
@group
.
full_path
}
/preview_markdown"
,
'markdownDocsPath'
=>
'/help/user/markdown'
,
'issuableTemplates'
=>
nil
,
...
...
spec/lib/banzai/cross_project_reference_spec.rb
View file @
761bdd32
...
...
@@ -3,20 +3,20 @@ require 'spec_helper'
describe
Banzai
::
CrossProjectReference
do
include
described_class
describe
'#p
rojec
t_from_ref'
do
describe
'#p
aren
t_from_ref'
do
context
'when no project was referenced'
do
it
'returns the project from context'
do
project
=
double
allow
(
self
).
to
receive
(
:context
).
and_return
({
project:
project
})
expect
(
p
rojec
t_from_ref
(
nil
)).
to
eq
project
expect
(
p
aren
t_from_ref
(
nil
)).
to
eq
project
end
end
context
'when referenced project does not exist'
do
it
'returns nil'
do
expect
(
p
rojec
t_from_ref
(
'invalid/reference'
)).
to
be_nil
expect
(
p
aren
t_from_ref
(
'invalid/reference'
)).
to
be_nil
end
end
...
...
@@ -27,7 +27,7 @@ describe Banzai::CrossProjectReference do
expect
(
Project
).
to
receive
(
:find_by_full_path
)
.
with
(
'cross/reference'
).
and_return
(
project2
)
expect
(
p
rojec
t_from_ref
(
'cross/reference'
)).
to
eq
project2
expect
(
p
aren
t_from_ref
(
'cross/reference'
)).
to
eq
project2
end
end
end
...
...
spec/lib/banzai/filter/abstract_reference_filter_spec.rb
View file @
761bdd32
...
...
@@ -3,67 +3,67 @@ require 'spec_helper'
describe
Banzai
::
Filter
::
AbstractReferenceFilter
do
let
(
:project
)
{
create
(
:project
)
}
describe
'#references_per_p
rojec
t'
do
it
'returns a Hash containing references grouped per p
rojec
t paths'
do
describe
'#references_per_p
aren
t'
do
it
'returns a Hash containing references grouped per p
aren
t paths'
do
doc
=
Nokogiri
::
HTML
.
fragment
(
"#1
#{
project
.
full_path
}
#2"
)
filter
=
described_class
.
new
(
doc
,
project:
project
)
expect
(
filter
).
to
receive
(
:object_class
).
exactly
(
4
).
times
.
and_return
(
Issue
)
expect
(
filter
).
to
receive
(
:object_sym
).
twice
.
and_return
(
:issue
)
refs
=
filter
.
references_per_p
rojec
t
refs
=
filter
.
references_per_p
aren
t
expect
(
refs
).
to
be_an_instance_of
(
Hash
)
expect
(
refs
[
project
.
full_path
]).
to
eq
(
Set
.
new
(
%w[1 2]
))
end
end
describe
'#p
rojects
_per_reference'
do
it
'returns a Hash containing projects grouped per p
rojec
t paths'
do
describe
'#p
arent
_per_reference'
do
it
'returns a Hash containing projects grouped per p
aren
t paths'
do
doc
=
Nokogiri
::
HTML
.
fragment
(
''
)
filter
=
described_class
.
new
(
doc
,
project:
project
)
expect
(
filter
).
to
receive
(
:references_per_p
rojec
t
)
expect
(
filter
).
to
receive
(
:references_per_p
aren
t
)
.
and_return
({
project
.
full_path
=>
Set
.
new
(
%w[1]
)
})
expect
(
filter
.
p
rojects
_per_reference
)
expect
(
filter
.
p
arent
_per_reference
)
.
to
eq
({
project
.
full_path
=>
project
})
end
end
describe
'#find_
projects_
for_paths'
do
describe
'#find_for_paths'
do
let
(
:doc
)
{
Nokogiri
::
HTML
.
fragment
(
''
)
}
let
(
:filter
)
{
described_class
.
new
(
doc
,
project:
project
)
}
context
'with RequestStore disabled'
do
it
'returns a list of Projects for a list of paths'
do
expect
(
filter
.
find_
projects_
for_paths
([
project
.
full_path
]))
expect
(
filter
.
find_for_paths
([
project
.
full_path
]))
.
to
eq
([
project
])
end
it
"return an empty array for paths that don't exist"
do
expect
(
filter
.
find_
projects_
for_paths
([
'nonexistent/project'
]))
expect
(
filter
.
find_for_paths
([
'nonexistent/project'
]))
.
to
eq
([])
end
end
context
'with RequestStore enabled'
,
:request_store
do
it
'returns a list of Projects for a list of paths'
do
expect
(
filter
.
find_
projects_
for_paths
([
project
.
full_path
]))
expect
(
filter
.
find_for_paths
([
project
.
full_path
]))
.
to
eq
([
project
])
end
context
"when no project with that path exists"
do
it
"returns no value"
do
expect
(
filter
.
find_
projects_
for_paths
([
'nonexistent/project'
]))
expect
(
filter
.
find_for_paths
([
'nonexistent/project'
]))
.
to
eq
([])
end
it
"adds the ref to the project refs cache"
do
project_refs_cache
=
{}
allow
(
filter
).
to
receive
(
:
project_
refs_cache
).
and_return
(
project_refs_cache
)
allow
(
filter
).
to
receive
(
:refs_cache
).
and_return
(
project_refs_cache
)
filter
.
find_
projects_
for_paths
([
'nonexistent/project'
])
filter
.
find_for_paths
([
'nonexistent/project'
])
expect
(
project_refs_cache
).
to
eq
({
'nonexistent/project'
=>
nil
})
end
...
...
@@ -71,11 +71,11 @@ describe Banzai::Filter::AbstractReferenceFilter do
context
'when the project refs cache includes nil values'
do
before
do
# adds { 'nonexistent/project' => nil } to cache
filter
.
project_
from_ref_cached
(
'nonexistent/project'
)
filter
.
from_ref_cached
(
'nonexistent/project'
)
end
it
"return an empty array for paths that don't exist"
do
expect
(
filter
.
find_
projects_
for_paths
([
'nonexistent/project'
]))
expect
(
filter
.
find_for_paths
([
'nonexistent/project'
]))
.
to
eq
([])
end
end
...
...
@@ -83,12 +83,12 @@ describe Banzai::Filter::AbstractReferenceFilter do
end
end
describe
'#current_p
rojec
t_path'
do
it
'returns the path of the current p
rojec
t'
do
describe
'#current_p
aren
t_path'
do
it
'returns the path of the current p
aren
t'
do
doc
=
Nokogiri
::
HTML
.
fragment
(
''
)
filter
=
described_class
.
new
(
doc
,
project:
project
)
expect
(
filter
.
current_p
rojec
t_path
).
to
eq
(
project
.
full_path
)
expect
(
filter
.
current_p
aren
t_path
).
to
eq
(
project
.
full_path
)
end
end
end
spec/lib/banzai/filter/issue_reference_filter_spec.rb
View file @
761bdd32
...
...
@@ -157,6 +157,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect
(
doc
.
text
).
to
eq
(
"Fixed (
#{
project2
.
full_path
}
#
#{
issue
.
iid
}
.)"
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
it
'ignores invalid issue IDs on the referenced project'
do
exp
=
act
=
"Fixed
#{
invalidate_reference
(
reference
)
}
"
...
...
@@ -201,6 +207,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect
(
doc
.
text
).
to
eq
(
"Fixed (
#{
project2
.
path
}
#
#{
issue
.
iid
}
.)"
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
it
'ignores invalid issue IDs on the referenced project'
do
exp
=
act
=
"Fixed
#{
invalidate_reference
(
reference
)
}
"
...
...
@@ -245,6 +257,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect
(
doc
.
text
).
to
eq
(
"Fixed (
#{
project2
.
path
}
#
#{
issue
.
iid
}
.)"
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
it
'ignores invalid issue IDs on the referenced project'
do
exp
=
act
=
"Fixed
#{
invalidate_reference
(
reference
)
}
"
...
...
@@ -269,8 +287,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it
'links with adjacent text'
do
doc
=
reference_filter
(
"Fixed (
#{
reference
}
.)"
)
expect
(
doc
.
to_html
).
to
match
(
/\(<a.+>
#{
Regexp
.
escape
(
issue
.
to_reference
(
project
))
}
\(comment 123\)<\/a>\.\)/
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
end
context
'cross-project reference in link href'
do
...
...
@@ -291,8 +316,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it
'links with adjacent text'
do
doc
=
reference_filter
(
"Fixed (
#{
reference_link
}
.)"
)
expect
(
doc
.
to_html
).
to
match
(
/\(<a.+>Reference<\/a>\.\)/
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference_link
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
end
context
'cross-project URL in link href'
do
...
...
@@ -313,8 +345,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it
'links with adjacent text'
do
doc
=
reference_filter
(
"Fixed (
#{
reference_link
}
.)"
)
expect
(
doc
.
to_html
).
to
match
(
/\(<a.+>Reference<\/a>\.\)/
)
end
it
'includes default classes'
do
doc
=
reference_filter
(
"Fixed (
#{
reference_link
}
.)"
)
expect
(
doc
.
css
(
'a'
).
first
.
attr
(
'class'
)).
to
eq
'gfm gfm-issue has-tooltip'
end
end
context
'group context'
do
...
...
@@ -387,19 +426,19 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
describe
'#
issues_per_projec
t'
do
describe
'#
records_per_paren
t'
do
context
'using an internal issue tracker'
do
it
'returns a Hash containing the issues per project'
do
doc
=
Nokogiri
::
HTML
.
fragment
(
''
)
filter
=
described_class
.
new
(
doc
,
project:
project
)
expect
(
filter
).
to
receive
(
:p
rojects
_per_reference
)
expect
(
filter
).
to
receive
(
:p
arent
_per_reference
)
.
and_return
({
project
.
full_path
=>
project
})
expect
(
filter
).
to
receive
(
:references_per_p
rojec
t
)
expect
(
filter
).
to
receive
(
:references_per_p
aren
t
)
.
and_return
({
project
.
full_path
=>
Set
.
new
([
issue
.
iid
])
})
expect
(
filter
.
issues_per_projec
t
)
expect
(
filter
.
records_per_paren
t
)
.
to
eq
({
project
=>
{
issue
.
iid
=>
issue
}
})
end
end
...
...
spec/lib/banzai/reference_parser/issue_parser_spec.rb
View file @
761bdd32
...
...
@@ -70,12 +70,12 @@ describe Banzai::ReferenceParser::IssueParser do
end
end
describe
'#
issue
s_for_nodes'
do
describe
'#
record
s_for_nodes'
do
it
'returns a Hash containing the issues for a list of nodes'
do
link
[
'data-issue'
]
=
issue
.
id
.
to_s
nodes
=
[
link
]
expect
(
subject
.
issue
s_for_nodes
(
nodes
)).
to
eq
({
link
=>
issue
})
expect
(
subject
.
record
s_for_nodes
(
nodes
)).
to
eq
({
link
=>
issue
})
end
end
end
spec/lib/gitlab/reference_extractor_spec.rb
View file @
761bdd32
require
'spec_helper'
describe
Gitlab
::
ReferenceExtractor
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:project
)
{
create
(
:project
,
group:
group
)
}
before
do
project
.
team
<<
[
project
.
creator
,
:developer
]
group
.
add_developer
(
project
.
creator
)
end
subject
{
described_class
.
new
(
project
,
project
.
creator
)
}
...
...
@@ -153,6 +154,20 @@ describe Gitlab::ReferenceExtractor do
expect
(
subject
.
snippets
).
to
match_array
([
@s0
,
@s1
])
end
it
'accesses valid epics'
do
stub_licensed_features
(
epics:
true
)
@e0
=
create
(
:epic
,
group:
group
)
@e1
=
create
(
:epic
,
group:
group
)
@e2
=
create
(
:epic
,
group:
create
(
:group
,
:private
))
text
=
"
#{
@e0
.
to_reference
(
group
)
}
, &999,
#{
@e1
.
to_reference
(
group
)
}
,
#{
@e2
.
to_reference
(
group
)
}
"
subject
.
analyze
(
text
,
{
group:
group
})
expect
(
subject
.
epics
).
to
match_array
([
@e0
,
@e1
])
end
it
'accesses valid commits'
do
project
=
create
(
:project
,
:repository
)
{
|
p
|
p
.
add_developer
(
p
.
creator
)
}
commit
=
project
.
commit
(
'master'
)
...
...
@@ -250,4 +265,34 @@ describe Gitlab::ReferenceExtractor do
subject
{
described_class
.
references_pattern
}
it
{
is_expected
.
to
be_kind_of
Regexp
}
end
describe
'referables prefixes'
do
def
prefixes
described_class
::
REFERABLES
.
each_with_object
({})
do
|
referable
,
result
|
klass
=
referable
.
to_s
.
camelize
.
constantize
next
unless
klass
.
respond_to?
(
:reference_prefix
)
prefix
=
klass
.
reference_prefix
result
[
prefix
]
||=
[]
result
[
prefix
]
<<
referable
end
end
it
'returns all supported prefixes'
do
expect
(
prefixes
.
keys
.
uniq
).
to
match_array
(
%w(@ # ~ % ! $ &)
)
end
it
'does not allow one prefix for multiple referables if not allowed specificly'
do
# make sure you are not overriding existing prefix before changing this hash
multiple_allowed
=
{
'@'
=>
3
}
prefixes
.
each
do
|
prefix
,
referables
|
expected_count
=
multiple_allowed
[
prefix
]
||
1
expect
(
referables
.
count
).
to
eq
(
expected_count
)
end
end
end
end
spec/support/markdown_feature.rb
View file @
761bdd32
...
...
@@ -79,6 +79,14 @@ class MarkdownFeature
@group_milestone
||=
create
(
:milestone
,
name:
'group-milestone'
,
group:
group
)
end
def
epic
@epic
||=
create
(
:epic
,
title:
'epic'
,
group:
group
)
end
def
epic_other_group
@epic
||=
create
(
:epic
,
title:
'epic'
)
end
# Cross-references -----------------------------------------------------------
def
xproject
...
...
spec/support/matchers/markdown_matchers.rb
View file @
761bdd32
...
...
@@ -159,6 +159,15 @@ module MarkdownMatchers
end
end
# EpicReferenceFilter
matcher
:reference_epics
do
set_default_markdown_messages
match
do
|
actual
|
expect
(
actual
).
to
have_selector
(
'a.gfm.gfm-epic'
,
count:
5
)
end
end
# TaskListFilter
matcher
:parse_task_lists
do
set_default_markdown_messages
...
...
spec/support/mentionable_shared_examples.rb
View file @
761bdd32
...
...
@@ -5,11 +5,13 @@
# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } }
shared_context
'mentionable context'
do
let
(
:group
)
{
create
(
:group
)
}
let
(
:project
)
{
subject
.
project
}
let
(
:author
)
{
subject
.
author
}
let
(
:mentioned_issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:mentioned_mr
)
{
create
(
:merge_request
,
source_project:
project
)
}
let
(
:mentioned_epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:mentioned_commit
)
{
project
.
commit
(
"HEAD~1"
)
}
let
(
:ext_proj
)
{
create
(
:project
,
:public
,
:repository
)
}
...
...
@@ -27,6 +29,7 @@ shared_context 'mentionable context' do
These references are new:
Issue:
#{
mentioned_issue
.
to_reference
}
Merge:
#{
mentioned_mr
.
to_reference
}
Epic:
#{
mentioned_epic
.
to_reference
(
project
)
}
Commit:
#{
mentioned_commit
.
to_reference
}
This reference is a repeat and should only be mentioned once:
...
...
@@ -43,6 +46,8 @@ shared_context 'mentionable context' do
end
before
do
stub_licensed_features
(
epics:
true
)
# Wire the project's repository to return the mentioned commit, and +nil+
# for any unrecognized commits.
allow_any_instance_of
(
::
Repository
).
to
receive
(
:commit
).
and_call_original
...
...
@@ -67,9 +72,10 @@ shared_examples 'a mentionable' do
it
"extracts references from its reference property"
do
# De-duplicate and omit itself
refs
=
subject
.
referenced_mentionables
expect
(
refs
.
size
).
to
eq
(
6
)
expect
(
refs
.
size
).
to
eq
(
7
)
expect
(
refs
).
to
include
(
mentioned_issue
)
expect
(
refs
).
to
include
(
mentioned_mr
)
expect
(
refs
).
to
include
(
mentioned_epic
)
expect
(
refs
).
to
include
(
mentioned_commit
)
expect
(
refs
).
to
include
(
ext_issue
)
expect
(
refs
).
to
include
(
ext_mr
)
...
...
@@ -77,7 +83,7 @@ shared_examples 'a mentionable' do
end
it
'creates cross-reference notes'
do
mentioned_objects
=
[
mentioned_issue
,
mentioned_mr
,
mentioned_commit
,
mentioned_objects
=
[
mentioned_issue
,
mentioned_mr
,
mentioned_
epic
,
mentioned_
commit
,
ext_issue
,
ext_mr
,
ext_commit
]
mentioned_objects
.
each
do
|
referenced
|
...
...
@@ -97,6 +103,7 @@ shared_examples 'an editable mentionable' do
let
(
:new_issues
)
do
[
create
(
:issue
,
project:
project
),
create
(
:issue
,
project:
ext_proj
)]
end
let
(
:new_epic
)
{
create
(
:epic
,
group:
group
)
}
it
'creates new cross-reference notes when the mentionable text is edited'
do
subject
.
save
...
...
@@ -107,6 +114,8 @@ shared_examples 'an editable mentionable' do
Issue:
#{
mentioned_issue
.
to_reference
}
Issue:
#{
mentioned_epic
.
to_reference
(
project
)
}
Commit:
#{
mentioned_commit
.
to_reference
}
---
...
...
@@ -117,23 +126,26 @@ shared_examples 'an editable mentionable' do
---
These t
wo
references are introduced in an edit:
These t
hree
references are introduced in an edit:
Issue:
#{
new_issues
[
0
].
to_reference
}
Cross:
#{
new_issues
[
1
].
to_reference
(
project
)
}
Epic:
#{
new_epic
.
to_reference
(
project
)
}
MSG
# These
three
objects were already referenced, and should not receive new
# These
four
objects were already referenced, and should not receive new
# notes
[
mentioned_issue
,
mentioned_commit
,
ext_issue
].
each
do
|
oldref
|
[
mentioned_issue
,
mentioned_commit
,
mentioned_epic
,
ext_issue
].
each
do
|
oldref
|
expect
(
SystemNoteService
).
not_to
receive
(
:cross_reference
)
.
with
(
oldref
,
any_args
)
end
# These two issues are new and should receive reference notes
# These two issues a
nd an epic a
re new and should receive reference notes
# In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
new_issues
.
each
do
|
newref
|
new_mentionables
=
new_issues
+
[
new_epic
]
new_mentionables
.
each
do
|
newref
|
expect
(
SystemNoteService
).
to
receive
(
:cross_reference
)
.
with
(
newref
,
subject
.
local_reference
,
author
)
end
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment