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
27d91a62
Commit
27d91a62
authored
Dec 10, 2019
by
GitLab Bot
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add latest changes from gitlab-org/gitlab@master
parent
5e11c9b7
Changes
62
Hide whitespace changes
Inline
Side-by-side
Showing
62 changed files
with
1070 additions
and
395 deletions
+1070
-395
app/assets/javascripts/pipelines/components/graph/graph_component.vue
...avascripts/pipelines/components/graph/graph_component.vue
+0
-4
app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
...ts/pipelines/components/graph/linked_pipelines_column.vue
+5
-2
app/assets/stylesheets/pages/diff.scss
app/assets/stylesheets/pages/diff.scss
+1
-0
app/controllers/admin/broadcast_messages_controller.rb
app/controllers/admin/broadcast_messages_controller.rb
+1
-0
app/models/broadcast_message.rb
app/models/broadcast_message.rb
+53
-23
app/models/commit.rb
app/models/commit.rb
+4
-0
app/models/commit_user_mention.rb
app/models/commit_user_mention.rb
+5
-0
app/models/concerns/mentionable.rb
app/models/concerns/mentionable.rb
+80
-0
app/models/issue.rb
app/models/issue.rb
+1
-0
app/models/issue_user_mention.rb
app/models/issue_user_mention.rb
+6
-0
app/models/merge_request.rb
app/models/merge_request.rb
+1
-0
app/models/merge_request_user_mention.rb
app/models/merge_request_user_mention.rb
+6
-0
app/models/note.rb
app/models/note.rb
+10
-0
app/models/snippet.rb
app/models/snippet.rb
+3
-0
app/models/snippet_user_mention.rb
app/models/snippet_user_mention.rb
+6
-0
app/models/user_mention.rb
app/models/user_mention.rb
+23
-0
app/services/create_snippet_service.rb
app/services/create_snippet_service.rb
+5
-1
app/services/issuable_base_service.rb
app/services/issuable_base_service.rb
+10
-2
app/services/notes/create_service.rb
app/services/notes/create_service.rb
+5
-1
app/services/notes/update_service.rb
app/services/notes/update_service.rb
+5
-1
app/services/update_snippet_service.rb
app/services/update_snippet_service.rb
+6
-2
app/views/search/_category.html.haml
app/views/search/_category.html.haml
+1
-1
app/views/shared/projects/_project.html.haml
app/views/shared/projects/_project.html.haml
+1
-1
changelogs/unreleased/21800-mentioned-users-models-with-array-type.yml
...released/21800-mentioned-users-models-with-array-type.yml
+5
-0
changelogs/unreleased/nicolasdular-add-broadcast-type.yml
changelogs/unreleased/nicolasdular-add-broadcast-type.yml
+5
-0
changelogs/unreleased/remove-downstream-node-lines.yml
changelogs/unreleased/remove-downstream-node-lines.yml
+5
-0
db/fixtures/development/03_project.rb
db/fixtures/development/03_project.rb
+4
-0
db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb
...20191129134844_add_broadcast_type_to_broadcast_message.rb
+19
-0
db/schema.rb
db/schema.rb
+1
-0
doc/ci/yaml/README.md
doc/ci/yaml/README.md
+1
-0
lib/banzai/reference_parser/mentioned_group_parser.rb
lib/banzai/reference_parser/mentioned_group_parser.rb
+1
-1
lib/banzai/reference_parser/mentioned_project_parser.rb
lib/banzai/reference_parser/mentioned_project_parser.rb
+1
-1
lib/gitlab/ci/config/entry/boolean.rb
lib/gitlab/ci/config/entry/boolean.rb
+0
-20
lib/gitlab/ci/config/entry/default.rb
lib/gitlab/ci/config/entry/default.rb
+8
-3
lib/gitlab/ci/config/entry/job.rb
lib/gitlab/ci/config/entry/job.rb
+5
-2
lib/gitlab/config/entry/array_of_strings.rb
lib/gitlab/config/entry/array_of_strings.rb
+18
-0
lib/gitlab/reference_extractor.rb
lib/gitlab/reference_extractor.rb
+1
-1
qa/qa/page/search/results.rb
qa/qa/page/search/results.rb
+17
-4
qa/qa/resource/api_fabricator.rb
qa/qa/resource/api_fabricator.rb
+14
-2
qa/qa/runtime/api/client.rb
qa/qa/runtime/api/client.rb
+17
-0
spec/frontend/environments/environment_item_spec.js
spec/frontend/environments/environment_item_spec.js
+131
-0
spec/frontend/environments/environment_table_spec.js
spec/frontend/environments/environment_table_spec.js
+65
-55
spec/frontend/environments/mock_data.js
spec/frontend/environments/mock_data.js
+106
-0
spec/javascripts/environments/environment_item_spec.js
spec/javascripts/environments/environment_item_spec.js
+0
-230
spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
...vascripts/pipelines/graph/linked_pipelines_column_spec.js
+4
-0
spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb
...ib/banzai/reference_parser/mentioned_group_parser_spec.rb
+1
-1
spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb
.../banzai/reference_parser/mentioned_project_parser_spec.rb
+1
-1
spec/lib/gitlab/ci/config/entry/default_spec.rb
spec/lib/gitlab/ci/config/entry/default_spec.rb
+1
-1
spec/lib/gitlab/ci/config/entry/job_spec.rb
spec/lib/gitlab/ci/config/entry/job_spec.rb
+1
-1
spec/lib/gitlab/ci/yaml_processor_spec.rb
spec/lib/gitlab/ci/yaml_processor_spec.rb
+2
-2
spec/lib/gitlab/import_export/all_models.yml
spec/lib/gitlab/import_export/all_models.yml
+4
-0
spec/models/broadcast_message_spec.rb
spec/models/broadcast_message_spec.rb
+87
-32
spec/models/concerns/mentionable_spec.rb
spec/models/concerns/mentionable_spec.rb
+67
-0
spec/models/issue_spec.rb
spec/models/issue_spec.rb
+1
-0
spec/models/merge_request_spec.rb
spec/models/merge_request_spec.rb
+1
-0
spec/models/snippet_spec.rb
spec/models/snippet_spec.rb
+1
-0
spec/models/user_mentions/commit_user_mention_spec.rb
spec/models/user_mentions/commit_user_mention_spec.rb
+11
-0
spec/models/user_mentions/issue_user_mention_spec.rb
spec/models/user_mentions/issue_user_mention_spec.rb
+12
-0
spec/models/user_mentions/merge_request_user_mention_spec.rb
spec/models/user_mentions/merge_request_user_mention_spec.rb
+12
-0
spec/models/user_mentions/snippet_user_mention_spec.rb
spec/models/user_mentions/snippet_user_mention_spec.rb
+12
-0
spec/support/shared_examples/mentionable_shared_examples.rb
spec/support/shared_examples/mentionable_shared_examples.rb
+150
-0
spec/support/shared_examples/models/user_mentions_shared_examples.rb
...t/shared_examples/models/user_mentions_shared_examples.rb
+40
-0
No files found.
app/assets/javascripts/pipelines/components/graph/graph_component.vue
View file @
27d91a62
...
...
@@ -100,9 +100,6 @@ export default {
hasOnlyOneJob
(
stage
)
{
return
stage
.
groups
.
length
===
1
;
},
hasDownstream
(
index
,
length
)
{
return
index
===
length
-
1
&&
this
.
hasTriggered
;
},
hasUpstream
(
index
)
{
return
index
===
0
&&
this
.
hasTriggeredBy
;
},
...
...
@@ -160,7 +157,6 @@ export default {
:key=
"stage.name"
:class=
"
{
'has-upstream prepend-left-64': hasUpstream(index),
'has-downstream': hasDownstream(index, graph.length),
'has-only-one-job': hasOnlyOneJob(stage),
'append-right-46': shouldAddRightMargin(index),
}"
...
...
app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
View file @
27d91a62
<
script
>
import
LinkedPipeline
from
'
./linked_pipeline.vue
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
...
...
@@ -27,6 +28,9 @@ export default {
};
return
`graph-position-
${
this
.
graphPosition
}
${
positionValues
[
this
.
graphPosition
]}
`
;
},
isUpstream
()
{
return
this
.
columnTitle
===
__
(
'
Upstream
'
);
},
},
};
</
script
>
...
...
@@ -34,13 +38,12 @@ export default {
<
template
>
<div
:class=
"columnClass"
class=
"stage-column linked-pipelines-column"
>
<div
class=
"stage-name linked-pipelines-column-title"
>
{{
columnTitle
}}
</div>
<div
class=
"cross-project-triangle"
></div>
<div
v-if=
"isUpstream"
class=
"cross-project-triangle"
></div>
<ul>
<linked-pipeline
v-for=
"(pipeline, index) in linkedPipelines"
:key=
"pipeline.id"
:class=
"
{
'flat-connector-before': index === 0
&&
graphPosition === 'right',
active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded
&&
graphPosition === 'left',
}"
...
...
app/assets/stylesheets/pages/diff.scss
View file @
27d91a62
...
...
@@ -473,6 +473,7 @@ table.code {
text-align
:
right
;
width
:
50px
;
position
:
relative
;
white-space
:
nowrap
;
a
{
transition
:
none
;
...
...
app/controllers/admin/broadcast_messages_controller.rb
View file @
27d91a62
...
...
@@ -61,6 +61,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
message
starts_at
target_path
broadcast_type
)
)
end
end
app/models/broadcast_message.rb
View file @
27d91a62
...
...
@@ -9,6 +9,7 @@ class BroadcastMessage < ApplicationRecord
validates
:message
,
presence:
true
validates
:starts_at
,
presence:
true
validates
:ends_at
,
presence:
true
validates
:broadcast_type
,
presence:
true
validates
:color
,
allow_blank:
true
,
color:
true
validates
:font
,
allow_blank:
true
,
color:
true
...
...
@@ -17,35 +18,62 @@ class BroadcastMessage < ApplicationRecord
default_value_for
:font
,
'#FFFFFF'
CACHE_KEY
=
'broadcast_message_current_json'
BANNER_CACHE_KEY
=
'broadcast_message_current_banner_json'
NOTIFICATION_CACHE_KEY
=
'broadcast_message_current_notification_json'
after_commit
:flush_redis_cache
def
self
.
current
(
current_path
=
nil
)
messages
=
cache
.
fetch
(
CACHE_KEY
,
as:
BroadcastMessage
,
expires_in:
cache_expires_in
)
do
current_and_future_messages
enum
broadcast_type:
{
banner:
1
,
notification:
2
}
class
<<
self
def
current_banner_messages
(
current_path
=
nil
)
fetch_messages
BANNER_CACHE_KEY
,
current_path
do
current_and_future_messages
.
banner
end
end
return
[]
unless
messages
&
.
present?
def
current_notification_messages
(
current_path
=
nil
)
fetch_messages
NOTIFICATION_CACHE_KEY
,
current_path
do
current_and_future_messages
.
notification
end
end
now_or_future
=
messages
.
select
(
&
:now_or_future?
)
def
current
(
current_path
=
nil
)
fetch_messages
CACHE_KEY
,
current_path
do
current_and_future_messages
end
end
# If there are cached entries but none are to be displayed we'll purge the
# cache so we don't keep running this code all the time.
cache
.
expire
(
CACHE_KEY
)
if
now_or_future
.
empty?
def
current_and_future_messages
where
(
'ends_at > :now'
,
now:
Time
.
current
).
order_id_asc
end
now_or_future
.
select
(
&
:now?
).
select
{
|
message
|
message
.
matches_current_path
(
current_path
)
}
end
def
cache
Gitlab
::
JsonCache
.
new
(
cache_key_with_version:
false
)
end
def
self
.
current_and_future_messages
where
(
'ends_at > :now'
,
now:
Time
.
zone
.
now
).
order_id_asc
end
def
cache_expires_in
2
.
weeks
end
def
self
.
cache
Gitlab
::
JsonCache
.
new
(
cache_key_with_version:
false
)
end
private
def
fetch_messages
(
cache_key
,
current_path
)
messages
=
cache
.
fetch
(
cache_key
,
as:
BroadcastMessage
,
expires_in:
cache_expires_in
)
do
yield
end
now_or_future
=
messages
.
select
(
&
:now_or_future?
)
def
self
.
cache_expires_in
2
.
weeks
# If there are cached entries but none are to be displayed we'll purge the
# cache so we don't keep running this code all the time.
cache
.
expire
(
cache_key
)
if
now_or_future
.
empty?
now_or_future
.
select
(
&
:now?
).
select
{
|
message
|
message
.
matches_current_path
(
current_path
)
}
end
end
def
active?
...
...
@@ -53,19 +81,19 @@ class BroadcastMessage < ApplicationRecord
end
def
started?
Time
.
zone
.
now
>=
starts_at
Time
.
current
>=
starts_at
end
def
ended?
ends_at
<
Time
.
zone
.
now
ends_at
<
Time
.
current
end
def
now?
(
starts_at
..
ends_at
).
cover?
(
Time
.
zone
.
now
)
(
starts_at
..
ends_at
).
cover?
(
Time
.
current
)
end
def
future?
starts_at
>
Time
.
zone
.
now
starts_at
>
Time
.
current
end
def
now_or_future?
...
...
@@ -79,7 +107,9 @@ class BroadcastMessage < ApplicationRecord
end
def
flush_redis_cache
self
.
class
.
cache
.
expire
(
CACHE_KEY
)
[
CACHE_KEY
,
BANNER_CACHE_KEY
,
NOTIFICATION_CACHE_KEY
].
each
do
|
key
|
self
.
class
.
cache
.
expire
(
key
)
end
end
end
...
...
app/models/commit.rb
View file @
27d91a62
...
...
@@ -281,6 +281,10 @@ class Commit
project
.
notes
.
for_commit_id
(
self
.
id
)
end
def
user_mentions
CommitUserMention
.
where
(
commit_id:
self
.
id
)
end
def
discussion_notes
notes
.
non_diff_notes
end
...
...
app/models/commit_user_mention.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
CommitUserMention
<
UserMention
belongs_to
:note
end
app/models/concerns/mentionable.rb
View file @
27d91a62
...
...
@@ -80,6 +80,66 @@ module Mentionable
all_references
(
current_user
).
users
end
def
store_mentions!
# if store_mentioned_users_to_db feature flag is not enabled then consider storing operation as succeeded
# because we wrap this method in transaction with with_transaction_returning_status, and we need the status to be
# successful if mentionable.save is successful.
#
# This line will get removed when we remove the feature flag.
return
true
unless
store_mentioned_users_to_db_enabled?
refs
=
all_references
(
self
.
author
)
references
=
{}
references
[
:mentioned_users_ids
]
=
refs
.
mentioned_users
&
.
pluck
(
:id
).
presence
references
[
:mentioned_groups_ids
]
=
refs
.
mentioned_groups
&
.
pluck
(
:id
).
presence
references
[
:mentioned_projects_ids
]
=
refs
.
mentioned_projects
&
.
pluck
(
:id
).
presence
# One retry should be enough as next time `model_user_mention` should return the existing mention record, that
# threw the `ActiveRecord::RecordNotUnique` exception in first place.
self
.
class
.
safe_ensure_unique
(
retries:
1
)
do
user_mention
=
model_user_mention
user_mention
.
mentioned_users_ids
=
references
[
:mentioned_users_ids
]
user_mention
.
mentioned_groups_ids
=
references
[
:mentioned_groups_ids
]
user_mention
.
mentioned_projects_ids
=
references
[
:mentioned_projects_ids
]
if
user_mention
.
has_mentions?
user_mention
.
save!
elsif
user_mention
.
persisted?
user_mention
.
destroy!
end
true
end
end
def
referenced_users
User
.
where
(
id:
user_mentions
.
select
(
"unnest(mentioned_users_ids)"
))
end
def
referenced_projects
(
current_user
=
nil
)
Project
.
where
(
id:
user_mentions
.
select
(
"unnest(mentioned_projects_ids)"
)).
public_or_visible_to_user
(
current_user
)
end
def
referenced_project_users
(
current_user
=
nil
)
User
.
joins
(
:project_members
).
where
(
members:
{
source_id:
referenced_projects
(
current_user
)
}).
distinct
end
def
referenced_groups
(
current_user
=
nil
)
# TODO: IMPORTANT: Revisit before using it.
# Check DB data for max mentioned groups per mentionable:
#
# select issue_id, count(mentions_count.men_gr_id) gr_count from
# (select DISTINCT unnest(mentioned_groups_ids) as men_gr_id, issue_id
# from issue_user_mentions group by issue_id, mentioned_groups_ids) as mentions_count
# group by mentions_count.issue_id order by gr_count desc limit 10
Group
.
where
(
id:
user_mentions
.
select
(
"unnest(mentioned_groups_ids)"
)).
public_or_visible_to_user
(
current_user
)
end
def
referenced_group_users
(
current_user
=
nil
)
User
.
joins
(
:group_members
).
where
(
members:
{
source_id:
referenced_groups
}).
distinct
end
def
directly_addressed_users
(
current_user
=
nil
)
all_references
(
current_user
).
directly_addressed_users
end
...
...
@@ -171,6 +231,26 @@ module Mentionable
def
mentionable_params
{}
end
# User mention that is parsed from model description rather then its related notes.
# Models that have a descriprion attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
# a description attribute.
#
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
# in a multithreaded environment. Make sure to use it within a *safe_ensure_unique* block.
def
model_user_mention
user_mentions
.
where
(
note_id:
nil
).
first_or_initialize
end
# We need this method to be checking that store_mentioned_users_to_db feature flag is enabled at the group level
# and not the project level as epics are defined at group level and we want to have epics store user mentions as well
# for the test period.
# During the test period the flag should be enabled at the group level.
def
store_mentioned_users_to_db_enabled?
return
Feature
.
enabled?
(
:store_mentioned_users_to_db
,
self
.
project
&
.
group
)
if
self
.
respond_to?
(
:project
)
return
Feature
.
enabled?
(
:store_mentioned_users_to_db
,
self
.
group
)
if
self
.
respond_to?
(
:group
)
end
end
Mentionable
.
prepend_if_ee
(
'EE::Mentionable'
)
app/models/issue.rb
View file @
27d91a62
...
...
@@ -42,6 +42,7 @@ class Issue < ApplicationRecord
has_many
:issue_assignees
has_many
:assignees
,
class_name:
"User"
,
through: :issue_assignees
has_many
:zoom_meetings
has_many
:user_mentions
,
class_name:
"IssueUserMention"
has_one
:sentry_issue
validates
:project
,
presence:
true
...
...
app/models/issue_user_mention.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
IssueUserMention
<
UserMention
belongs_to
:issue
belongs_to
:note
end
app/models/merge_request.rb
View file @
27d91a62
...
...
@@ -71,6 +71,7 @@ class MergeRequest < ApplicationRecord
has_many
:merge_request_assignees
has_many
:assignees
,
class_name:
"User"
,
through: :merge_request_assignees
has_many
:user_mentions
,
class_name:
"MergeRequestUserMention"
has_many
:deployment_merge_requests
...
...
app/models/merge_request_user_mention.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
MergeRequestUserMention
<
UserMention
belongs_to
:merge_request
belongs_to
:note
end
app/models/note.rb
View file @
27d91a62
...
...
@@ -499,8 +499,18 @@ class Note < ApplicationRecord
project
end
def
user_mentions
noteable
.
user_mentions
.
where
(
note:
self
)
end
private
# Using this method followed by a call to `save` may result in ActiveRecord::RecordNotUnique exception
# in a multithreaded environment. Make sure to use it within a `safe_ensure_unique` block.
def
model_user_mention
user_mentions
.
first_or_initialize
end
def
system_note_viewable_by?
(
user
)
return
true
unless
system_note_metadata
...
...
app/models/snippet.rb
View file @
27d91a62
...
...
@@ -37,6 +37,7 @@ class Snippet < ApplicationRecord
belongs_to
:project
has_many
:notes
,
as: :noteable
,
dependent: :destroy
# rubocop:disable Cop/ActiveRecordDependent
has_many
:user_mentions
,
class_name:
"SnippetUserMention"
delegate
:name
,
:email
,
to: :author
,
prefix:
true
,
allow_nil:
true
...
...
@@ -69,6 +70,8 @@ class Snippet < ApplicationRecord
scope
:inc_author
,
->
{
includes
(
:author
)
}
scope
:inc_relations_for_view
,
->
{
includes
(
author: :status
)
}
attr_mentionable
:description
participant
:author
participant
:notes_with_associations
...
...
app/models/snippet_user_mention.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
SnippetUserMention
<
UserMention
belongs_to
:snippet
belongs_to
:note
end
app/models/user_mention.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
UserMention
<
ApplicationRecord
self
.
abstract_class
=
true
def
has_mentions?
mentioned_users_ids
.
present?
||
mentioned_groups_ids
.
present?
||
mentioned_projects_ids
.
present?
end
private
def
mentioned_users
User
.
where
(
id:
mentioned_users_ids
)
end
def
mentioned_groups
Group
.
where
(
id:
mentioned_groups_ids
)
end
def
mentioned_projects
Project
.
where
(
id:
mentioned_projects_ids
)
end
end
app/services/create_snippet_service.rb
View file @
27d91a62
...
...
@@ -21,7 +21,11 @@ class CreateSnippetService < BaseService
spam_check
(
snippet
,
current_user
)
if
snippet
.
save
snippet_saved
=
snippet
.
with_transaction_returning_status
do
snippet
.
save
&&
snippet
.
store_mentions!
end
if
snippet_saved
UserAgentDetailService
.
new
(
snippet
,
@request
).
create
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:create
)
end
...
...
app/services/issuable_base_service.rb
View file @
27d91a62
...
...
@@ -163,7 +163,11 @@ class IssuableBaseService < BaseService
before_create
(
issuable
)
if
issuable
.
save
issuable_saved
=
issuable
.
with_transaction_returning_status
do
issuable
.
save
&&
issuable
.
store_mentions!
end
if
issuable_saved
Issuable
::
CommonSystemNotesService
.
new
(
project
,
current_user
).
execute
(
issuable
,
is_update:
false
)
after_create
(
issuable
)
...
...
@@ -224,7 +228,11 @@ class IssuableBaseService < BaseService
update_project_counters
=
issuable
.
project
&&
update_project_counter_caches?
(
issuable
)
ensure_milestone_available
(
issuable
)
if
issuable
.
with_transaction_returning_status
{
issuable
.
save
(
touch:
should_touch
)
}
issuable_saved
=
issuable
.
with_transaction_returning_status
do
issuable
.
save
(
touch:
should_touch
)
&&
issuable
.
store_mentions!
end
if
issuable_saved
Issuable
::
CommonSystemNotesService
.
new
(
project
,
current_user
).
execute
(
issuable
,
old_labels:
old_associations
[
:labels
])
handle_changes
(
issuable
,
old_associations:
old_associations
)
...
...
app/services/notes/create_service.rb
View file @
27d91a62
...
...
@@ -33,7 +33,11 @@ module Notes
NewNoteWorker
.
perform_async
(
note
.
id
)
end
if
!
only_commands
&&
note
.
save
note_saved
=
note
.
with_transaction_returning_status
do
!
only_commands
&&
note
.
save
&&
note
.
store_mentions!
end
if
note_saved
if
note
.
part_of_discussion?
&&
note
.
discussion
.
can_convert_to_discussion?
note
.
discussion
.
convert_to_discussion!
(
save:
true
)
end
...
...
app/services/notes/update_service.rb
View file @
27d91a62
...
...
@@ -7,7 +7,11 @@ module Notes
old_mentioned_users
=
note
.
mentioned_users
(
current_user
).
to_a
note
.
update
(
params
.
merge
(
updated_by:
current_user
))
note
.
assign_attributes
(
params
.
merge
(
updated_by:
current_user
))
note
.
with_transaction_returning_status
do
note
.
save
&&
note
.
store_mentions!
end
only_commands
=
false
...
...
app/services/update_snippet_service.rb
View file @
27d91a62
...
...
@@ -25,8 +25,12 @@ class UpdateSnippetService < BaseService
snippet
.
assign_attributes
(
params
)
spam_check
(
snippet
,
current_user
)
snippet
.
save
.
tap
do
|
succeeded
|
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:update
)
if
succeeded
snippet_saved
=
snippet
.
with_transaction_returning_status
do
snippet
.
save
&&
snippet
.
store_mentions!
end
if
snippet_saved
Gitlab
::
UsageDataCounters
::
SnippetCounter
.
count
(
:update
)
end
end
end
app/views/search/_category.html.haml
View file @
27d91a62
...
...
@@ -27,7 +27,7 @@
=
search_filter_link
'snippet_blobs'
,
_
(
"Snippet Contents"
),
search:
{
snippets:
true
,
group_id:
nil
,
project_id:
nil
}
=
search_filter_link
'snippet_titles'
,
_
(
"Titles and Filenames"
),
search:
{
snippets:
true
,
group_id:
nil
,
project_id:
nil
}
-
else
=
search_filter_link
'projects'
,
_
(
"Projects"
)
=
search_filter_link
'projects'
,
_
(
"Projects"
)
,
data:
{
qa_selector:
'projects_tab'
}
=
search_filter_link
'issues'
,
_
(
"Issues"
)
=
search_filter_link
'merge_requests'
,
_
(
"Merge requests"
)
=
search_filter_link
'milestones'
,
_
(
"Milestones"
)
...
...
app/views/shared/projects/_project.html.haml
View file @
27d91a62
...
...
@@ -26,7 +26,7 @@
=
image_tag
avatar_icon_for_user
(
project
.
creator
,
48
),
class:
"avatar s48"
,
alt
:''
-
else
=
project_icon
(
project
,
alt:
''
,
class:
'avatar project-avatar s48'
,
width:
48
,
height:
48
)
.project-details.d-sm-flex.flex-sm-fill.align-items-center
.project-details.d-sm-flex.flex-sm-fill.align-items-center
{
data:
{
qa_selector:
'project'
,
qa_project_name:
project
.
name
}
}
.flex-wrapper
.d-flex.align-items-center.flex-wrap.project-title
%h2
.d-flex.prepend-top-8
...
...
changelogs/unreleased/21800-mentioned-users-models-with-array-type.yml
0 → 100644
View file @
27d91a62
---
title
:
Store users, groups, projects mentioned in Markdown to DB tables
merge_request
:
19088
author
:
type
:
added
changelogs/unreleased/nicolasdular-add-broadcast-type.yml
0 → 100644
View file @
27d91a62
---
title
:
Add type to broadcast messages
merge_request
:
21038
author
:
type
:
added
changelogs/unreleased/remove-downstream-node-lines.yml
0 → 100644
View file @
27d91a62
---
title
:
Remove downstream pipeline connecting lines
merge_request
:
21196
author
:
type
:
removed
db/fixtures/development/03_project.rb
View file @
27d91a62
...
...
@@ -141,6 +141,10 @@ class Gitlab::Seeder::Projects
# the `after_commit` queue to ensure the job is run now.
project
.
send
(
:_run_after_commit_queue
)
project
.
import_state
.
send
(
:_run_after_commit_queue
)
# Expire repository cache after import to ensure
# valid_repo? call below returns a correct answer
project
.
repository
.
expire_all_method_caches
end
if
project
.
valid?
&&
project
.
valid_repo?
...
...
db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
class
AddBroadcastTypeToBroadcastMessage
<
ActiveRecord
::
Migration
[
5.2
]
include
Gitlab
::
Database
::
MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME
=
false
BROADCAST_MESSAGE_BANNER_TYPE
=
1
disable_ddl_transaction!
def
up
add_column_with_default
(
:broadcast_messages
,
:broadcast_type
,
:smallint
,
default:
BROADCAST_MESSAGE_BANNER_TYPE
)
end
def
down
remove_column
(
:broadcast_messages
,
:broadcast_type
)
end
end
db/schema.rb
View file @
27d91a62
...
...
@@ -575,6 +575,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
t
.
text
"message_html"
,
null:
false
t
.
integer
"cached_markdown_version"
t
.
string
"target_path"
,
limit:
255
t
.
integer
"broadcast_type"
,
limit:
2
,
default:
1
,
null:
false
t
.
index
[
"starts_at"
,
"ends_at"
,
"id"
],
name:
"index_broadcast_messages_on_starts_at_and_ends_at_and_id"
end
...
...
doc/ci/yaml/README.md
View file @
27d91a62
...
...
@@ -135,6 +135,7 @@ The following job parameters can be defined inside a `default:` block:
-
[
`services`
](
#services
)
-
[
`before_script`
](
#before_script-and-after_script
)
-
[
`after_script`
](
#before_script-and-after_script
)
-
[
`tags`
](
#tags
)
-
[
`cache`
](
#cache
)
-
[
`retry`
](
#retry
)
-
[
`timeout`
](
#timeout
)
...
...
lib/banzai/reference_parser/mentioned_
users_by_
group_parser.rb
→
lib/banzai/reference_parser/mentioned_group_parser.rb
View file @
27d91a62
...
...
@@ -2,7 +2,7 @@
module
Banzai
module
ReferenceParser
class
Mentioned
UsersBy
GroupParser
<
BaseParser
class
MentionedGroupParser
<
BaseParser
GROUP_ATTR
=
'data-group'
self
.
reference_type
=
:user
...
...
lib/banzai/reference_parser/mentioned_
users_by_
project_parser.rb
→
lib/banzai/reference_parser/mentioned_project_parser.rb
View file @
27d91a62
...
...
@@ -2,7 +2,7 @@
module
Banzai
module
ReferenceParser
class
Mentioned
UsersBy
ProjectParser
<
ProjectParser
class
MentionedProjectParser
<
ProjectParser
PROJECT_ATTR
=
'data-project'
self
.
reference_type
=
:user
...
...
lib/gitlab/ci/config/entry/boolean.rb
deleted
100644 → 0
View file @
5e11c9b7
# frozen_string_literal: true
module
Gitlab
module
Ci
class
Config
module
Entry
##
# Entry that represents the interrutible value.
#
class
Boolean
<
::
Gitlab
::
Config
::
Entry
::
Node
include
::
Gitlab
::
Config
::
Entry
::
Validatable
validations
do
validates
:config
,
boolean:
true
end
end
end
end
end
end
lib/gitlab/ci/config/entry/default.rb
View file @
27d91a62
...
...
@@ -15,7 +15,7 @@ module Gitlab
ALLOWED_KEYS
=
%i[before_script image services
after_script cache interruptible
timeout retry]
.
freeze
timeout retry
tags
]
.
freeze
validations
do
validates
:config
,
allowed_keys:
ALLOWED_KEYS
...
...
@@ -41,7 +41,7 @@ module Gitlab
description:
'Configure caching between build jobs.'
,
inherit:
true
entry
:interruptible
,
Entry
::
Boolean
,
entry
:interruptible
,
::
Gitlab
::
Config
::
Entry
::
Boolean
,
description:
'Set jobs interruptible default value.'
,
inherit:
false
...
...
@@ -53,7 +53,12 @@ module Gitlab
description:
'Set retry default value.'
,
inherit:
false
helpers
:before_script
,
:image
,
:services
,
:after_script
,
:cache
,
:interruptible
,
:timeout
,
:retry
entry
:tags
,
::
Gitlab
::
Config
::
Entry
::
ArrayOfStrings
,
description:
'Set the default tags.'
,
inherit:
false
helpers
:before_script
,
:image
,
:services
,
:after_script
,
:cache
,
:interruptible
,
:timeout
,
:retry
,
:tags
private
...
...
lib/gitlab/ci/config/entry/job.rb
View file @
27d91a62
...
...
@@ -36,7 +36,6 @@ module Gitlab
if: :has_rules?
with_options
allow_nil:
true
do
validates
:tags
,
array_of_strings:
true
validates
:allow_failure
,
boolean:
true
validates
:parallel
,
numericality:
{
only_integer:
true
,
greater_than_or_equal_to:
2
,
...
...
@@ -97,7 +96,7 @@ module Gitlab
description:
'Services that will be used to execute this job.'
,
inherit:
true
entry
:interruptible
,
Entry
::
Boolean
,
entry
:interruptible
,
::
Gitlab
::
Config
::
Entry
::
Boolean
,
description:
'Set jobs interruptible value.'
,
inherit:
true
...
...
@@ -109,6 +108,10 @@ module Gitlab
description:
'Retry configuration for this job.'
,
inherit:
true
entry
:tags
,
::
Gitlab
::
Config
::
Entry
::
ArrayOfStrings
,
description:
'Set the tags.'
,
inherit:
true
entry
:only
,
Entry
::
Policy
,
description:
'Refs policy this job will be executed for.'
,
default:
Entry
::
Policy
::
DEFAULT_ONLY
,
...
...
lib/gitlab/config/entry/array_of_strings.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
module
Gitlab
module
Config
module
Entry
##
# Entry that represents a array of strings value.
#
class
ArrayOfStrings
<
Node
include
Validatable
validations
do
validates
:config
,
array_of_strings:
true
end
end
end
end
end
lib/gitlab/reference_extractor.rb
View file @
27d91a62
...
...
@@ -3,7 +3,7 @@
module
Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class
ReferenceExtractor
<
Banzai
::
ReferenceExtractor
REFERABLES
=
%i(user issue label milestone
REFERABLES
=
%i(user issue label milestone
mentioned_user mentioned_group mentioned_project
merge_request snippet commit commit_range directly_addressed_user epic)
.
freeze
attr_accessor
:project
,
:current_user
,
:author
...
...
qa/qa/page/search/results.rb
View file @
27d91a62
...
...
@@ -5,6 +5,7 @@ module QA::Page
class
Results
<
QA
::
Page
::
Base
view
'app/views/search/_category.html.haml'
do
element
:code_tab
element
:projects_tab
end
view
'app/views/search/results/_blob_data.html.haml'
do
...
...
@@ -13,21 +14,33 @@ module QA::Page
element
:file_text_content
end
view
'app/views/shared/projects/_project.html.haml'
do
element
:project
end
def
switch_to_code
click_element
(
:code_tab
)
end
def
switch_to_projects
click_element
(
:projects_tab
)
end
def
has_file_in_project?
(
file_name
,
project_name
)
has_element?
:result_item_content
,
text:
"
#{
project_name
}
:
#{
file_name
}
"
has_element?
(
:result_item_content
,
text:
"
#{
project_name
}
:
#{
file_name
}
"
)
end
def
has_file_with_content?
(
file_name
,
file_text
)
within_element_by_index
:result_item_content
,
0
do
false
unless
has_element?
:file_title_content
,
text:
file_name
within_element_by_index
(
:result_item_content
,
0
)
do
false
unless
has_element?
(
:file_title_content
,
text:
file_name
)
has_element?
:file_text_content
,
text:
file_text
has_element?
(
:file_text_content
,
text:
file_text
)
end
end
def
has_project?
(
project_name
)
has_element?
(
:project
,
project_name:
project_name
)
end
end
end
end
qa/qa/resource/api_fabricator.rb
View file @
27d91a62
...
...
@@ -19,8 +19,8 @@ module QA
def
api_support?
respond_to?
(
:api_get_path
)
&&
respond_to?
(
:api_post_path
)
&&
respond_to?
(
:api_post_body
)
(
respond_to?
(
:api_post_path
)
&&
respond_to?
(
:api_post_body
))
||
(
respond_to?
(
:api_put_path
)
&&
respond_to?
(
:api_put_body
)
)
end
def
fabricate_via_api!
...
...
@@ -84,6 +84,18 @@ module QA
process_api_response
(
parse_body
(
response
))
end
def
api_put
response
=
put
(
Runtime
::
API
::
Request
.
new
(
api_client
,
api_put_path
).
url
,
api_put_body
)
unless
response
.
code
==
HTTP_STATUS_OK
raise
ResourceFabricationFailedError
,
"Updating
#{
self
.
class
.
name
}
using the API failed (
#{
response
.
code
}
) with `
#{
response
}
`."
end
process_api_response
(
parse_body
(
response
))
end
def
api_delete
url
=
Runtime
::
API
::
Request
.
new
(
api_client
,
api_delete_path
).
url
response
=
delete
(
url
)
...
...
qa/qa/runtime/api/client.rb
View file @
27d91a62
...
...
@@ -25,6 +25,23 @@ module QA
end
end
def
self
.
as_admin
if
Runtime
::
Env
.
admin_personal_access_token
Runtime
::
API
::
Client
.
new
(
:gitlab
,
personal_access_token:
Runtime
::
Env
.
admin_personal_access_token
)
else
user
=
Resource
::
User
.
fabricate_via_api!
do
|
user
|
user
.
username
=
Runtime
::
User
.
admin_username
user
.
password
=
Runtime
::
User
.
admin_password
end
unless
user
.
admin?
raise
AuthorizationError
,
"User '
#{
user
.
username
}
' is not an administrator."
end
Runtime
::
API
::
Client
.
new
(
:gitlab
,
user:
user
)
end
end
private
def
enable_ip_limits
...
...
spec/frontend/environments/environment_item_spec.js
0 → 100644
View file @
27d91a62
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
format
}
from
'
timeago.js
'
;
import
EnvironmentItem
from
'
~/environments/components/environment_item.vue
'
;
import
{
environment
,
folder
,
tableData
}
from
'
./mock_data
'
;
describe
(
'
Environment item
'
,
()
=>
{
let
wrapper
;
const
factory
=
(
options
=
{})
=>
{
// This destroys any wrappers created before a nested call to factory reassigns it
if
(
wrapper
&&
wrapper
.
destroy
)
{
wrapper
.
destroy
();
}
wrapper
=
mount
(
EnvironmentItem
,
{
...
options
,
});
};
beforeEach
(()
=>
{
factory
({
propsData
:
{
model
:
environment
,
canReadEnvironment
:
true
,
tableData
,
},
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
when item is not folder
'
,
()
=>
{
it
(
'
should render environment name
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.environment-name
'
).
text
()).
toContain
(
environment
.
name
);
});
describe
(
'
With deployment
'
,
()
=>
{
it
(
'
should render deployment internal id
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.deployment-column span
'
).
text
()).
toContain
(
environment
.
last_deployment
.
iid
,
);
expect
(
wrapper
.
find
(
'
.deployment-column span
'
).
text
()).
toContain
(
'
#
'
);
});
it
(
'
should render last deployment date
'
,
()
=>
{
const
formatedDate
=
format
(
environment
.
last_deployment
.
deployed_at
);
expect
(
wrapper
.
find
(
'
.environment-created-date-timeago
'
).
text
()).
toContain
(
formatedDate
);
});
describe
(
'
With user information
'
,
()
=>
{
it
(
'
should render user avatar with link to profile
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-deploy-user-container
'
).
attributes
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
user
.
web_url
,
);
});
});
describe
(
'
With build url
'
,
()
=>
{
it
(
'
should link to build url provided
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.build-link
'
).
attributes
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
it
(
'
should render deployable name and id
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.build-link
'
).
attributes
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
});
describe
(
'
With commit information
'
,
()
=>
{
it
(
'
should render commit component
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-commit-component
'
)).
toBeDefined
();
});
});
});
describe
(
'
With manual actions
'
,
()
=>
{
it
(
'
should render actions component
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-manual-actions-container
'
)).
toBeDefined
();
});
});
describe
(
'
With external URL
'
,
()
=>
{
it
(
'
should render external url component
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-external-url-container
'
)).
toBeDefined
();
});
});
describe
(
'
With stop action
'
,
()
=>
{
it
(
'
should render stop action component
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-stop-component-container
'
)).
toBeDefined
();
});
});
describe
(
'
With retry action
'
,
()
=>
{
it
(
'
should render rollback component
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-rollback-component-container
'
)).
toBeDefined
();
});
});
});
describe
(
'
When item is folder
'
,
()
=>
{
beforeEach
(()
=>
{
factory
({
propsData
:
{
model
:
folder
,
canReadEnvironment
:
true
,
tableData
,
},
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
should render folder icon and name
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.folder-name
'
).
text
()).
toContain
(
folder
.
name
);
expect
(
wrapper
.
find
(
'
.folder-icon
'
)).
toBeDefined
();
});
it
(
'
should render the number of children in a badge
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.folder-name .badge
'
).
text
()).
toContain
(
folder
.
size
);
});
});
});
spec/
javascripts
/environments/environment_table_spec.js
→
spec/
frontend
/environments/environment_table_spec.js
View file @
27d91a62
import
Vue
from
'
vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
environmentTableComp
from
'
~/environments/components/environments_table.vue
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
EnvironmentTable
from
'
~/environments/components/environments_table.vue
'
;
import
{
folder
}
from
'
./mock_data
'
;
const
eeOnlyProps
=
{
canaryDeploymentFeatureId
:
'
canary_deployment
'
,
showCanaryDeploymentCallout
:
true
,
userCalloutsPath
:
'
/callouts
'
,
lockPromotionSvgPath
:
'
/assets/illustrations/lock-promotion.svg
'
,
helpCanaryDeploymentsPath
:
'
help/canary-deployments
'
,
};
describe
(
'
Environment table
'
,
()
=>
{
let
Component
;
let
vm
;
let
wrapper
;
const
eeOnlyProps
=
{
canaryDeploymentFeatureId
:
'
canary_deployment
'
,
showCanaryDeploymentCallout
:
true
,
userCalloutsPath
:
'
/callouts
'
,
lockPromotionSvgPath
:
'
/assets/illustrations/lock-promotion.svg
'
,
helpCanaryDeploymentsPath
:
'
help/canary-deployments
'
,
const
factory
=
(
options
=
{})
=>
{
// This destroys any wrappers created before a nested call to factory reassigns it
if
(
wrapper
&&
wrapper
.
destroy
)
{
wrapper
.
destroy
();
}
wrapper
=
mount
(
EnvironmentTable
,
{
...
options
,
});
};
beforeEach
(()
=>
{
Component
=
Vue
.
extend
(
environmentTableComp
);
factory
({
propsData
:
{
environments
:
[
folder
],
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
});
afterEach
(()
=>
{
vm
.
$
destroy
();
wrapper
.
destroy
();
});
it
(
'
Should render a table
'
,
()
=>
{
const
mockItem
=
{
name
:
'
review
'
,
size
:
3
,
isFolder
:
true
,
latest
:
{
environment_path
:
'
url
'
,
},
};
vm
=
mountComponent
(
Component
,
{
environments
:
[
mockItem
],
canReadEnvironment
:
true
,
...
eeOnlyProps
,
});
expect
(
vm
.
$el
.
getAttribute
(
'
class
'
)).
toContain
(
'
ci-table
'
);
expect
(
wrapper
.
classes
()).
toContain
(
'
ci-table
'
);
});
describe
(
'
sortEnvironments
'
,
()
=>
{
...
...
@@ -73,15 +73,17 @@ describe('Environment table', () => {
},
];
vm
=
mountComponent
(
Component
,
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
factory
({
propsData
:
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
const
[
old
,
newer
,
older
,
noDeploy
]
=
mockItems
;
expect
(
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
newer
,
old
,
older
,
noDeploy
]);
expect
(
wrapper
.
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
newer
,
old
,
older
,
noDeploy
]);
});
it
(
'
should push environments with no deployments to the bottom
'
,
()
=>
{
...
...
@@ -137,15 +139,17 @@ describe('Environment table', () => {
},
];
vm
=
mountComponent
(
Component
,
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
factory
({
propsData
:
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
const
[
prod
,
review
,
staging
]
=
mockItems
;
expect
(
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
review
,
staging
,
prod
]);
expect
(
wrapper
.
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
review
,
staging
,
prod
]);
});
it
(
'
should sort environments by folder first
'
,
()
=>
{
...
...
@@ -174,15 +178,17 @@ describe('Environment table', () => {
},
];
vm
=
mountComponent
(
Component
,
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
factory
({
propsData
:
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
const
[
old
,
newer
,
older
]
=
mockItems
;
expect
(
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
older
,
newer
,
old
]);
expect
(
wrapper
.
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
older
,
newer
,
old
]);
});
it
(
'
should break ties by name
'
,
()
=>
{
...
...
@@ -201,15 +207,17 @@ describe('Environment table', () => {
},
];
vm
=
mountComponent
(
Component
,
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
factory
({
propsData
:
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
const
[
old
,
newer
,
older
]
=
mockItems
;
expect
(
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
older
,
newer
,
old
]);
expect
(
wrapper
.
vm
.
sortEnvironments
(
mockItems
)).
toEqual
([
older
,
newer
,
old
]);
});
});
...
...
@@ -250,19 +258,21 @@ describe('Environment table', () => {
const
[
production
,
review
,
staging
]
=
mockItems
;
const
[
addcibuildstatus
,
master
]
=
mockItems
[
1
].
children
;
vm
=
mountComponent
(
Component
,
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
factory
({
propsData
:
{
environments
:
mockItems
,
canReadEnvironment
:
true
,
...
eeOnlyProps
,
},
});
expect
(
vm
.
sortedEnvironments
.
map
(
env
=>
env
.
name
)).
toEqual
([
expect
(
wrapper
.
vm
.
sortedEnvironments
.
map
(
env
=>
env
.
name
)).
toEqual
([
review
.
name
,
staging
.
name
,
production
.
name
,
]);
expect
(
vm
.
sortedEnvironments
[
0
].
children
).
toEqual
([
master
,
addcibuildstatus
]);
expect
(
wrapper
.
vm
.
sortedEnvironments
[
0
].
children
).
toEqual
([
master
,
addcibuildstatus
]);
});
});
});
spec/frontend/environments/mock_data.js
0 → 100644
View file @
27d91a62
const
environment
=
{
name
:
'
production
'
,
size
:
1
,
state
:
'
stopped
'
,
external_url
:
'
http://external.com
'
,
environment_type
:
null
,
last_deployment
:
{
id
:
66
,
iid
:
6
,
sha
:
'
500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
ref
:
{
name
:
'
master
'
,
ref_url
:
'
root/ci-folders/tree/master
'
,
},
tag
:
true
,
'
last?
'
:
true
,
user
:
{
name
:
'
Administrator
'
,
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit
:
{
id
:
'
500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
short_id
:
'
500aabcb
'
,
title
:
'
Update .gitlab-ci.yml
'
,
author_name
:
'
Administrator
'
,
author_email
:
'
admin@example.com
'
,
created_at
:
'
2016-11-07T18:28:13.000+00:00
'
,
message
:
'
Update .gitlab-ci.yml
'
,
author
:
{
name
:
'
Administrator
'
,
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit_path
:
'
/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
},
deployable
:
{
id
:
1279
,
name
:
'
deploy
'
,
build_path
:
'
/root/ci-folders/builds/1279
'
,
retry_path
:
'
/root/ci-folders/builds/1279/retry
'
,
created_at
:
'
2016-11-29T18:11:58.430Z
'
,
updated_at
:
'
2016-11-29T18:11:58.430Z
'
,
},
manual_actions
:
[
{
name
:
'
action
'
,
play_path
:
'
/play
'
,
},
],
deployed_at
:
'
2016-11-29T18:11:58.430Z
'
,
},
has_stop_action
:
true
,
environment_path
:
'
root/ci-folders/environments/31
'
,
log_path
:
'
root/ci-folders/environments/31/logs
'
,
created_at
:
'
2016-11-07T11:11:16.525Z
'
,
updated_at
:
'
2016-11-10T15:55:58.778Z
'
,
};
const
folder
=
{
name
:
'
review
'
,
folderName
:
'
review
'
,
size
:
3
,
isFolder
:
true
,
environment_path
:
'
url
'
,
log_path
:
'
url
'
,
latest
:
{
environment_path
:
'
url
'
,
},
};
const
tableData
=
{
name
:
{
title
:
'
Environment
'
,
spacing
:
'
section-15
'
,
},
deploy
:
{
title
:
'
Deployment
'
,
spacing
:
'
section-10
'
,
},
build
:
{
title
:
'
Job
'
,
spacing
:
'
section-15
'
,
},
commit
:
{
title
:
'
Commit
'
,
spacing
:
'
section-20
'
,
},
date
:
{
title
:
'
Updated
'
,
spacing
:
'
section-10
'
,
},
actions
:
{
spacing
:
'
section-25
'
,
},
};
export
{
environment
,
folder
,
tableData
};
spec/javascripts/environments/environment_item_spec.js
deleted
100644 → 0
View file @
5e11c9b7
import
{
format
}
from
'
timeago.js
'
;
import
Vue
from
'
vue
'
;
import
environmentItemComp
from
'
~/environments/components/environment_item.vue
'
;
const
tableData
=
{
name
:
{
title
:
'
Environment
'
,
spacing
:
'
section-15
'
,
},
deploy
:
{
title
:
'
Deployment
'
,
spacing
:
'
section-10
'
,
},
build
:
{
title
:
'
Job
'
,
spacing
:
'
section-15
'
,
},
commit
:
{
title
:
'
Commit
'
,
spacing
:
'
section-20
'
,
},
date
:
{
title
:
'
Updated
'
,
spacing
:
'
section-10
'
,
},
actions
:
{
spacing
:
'
section-25
'
,
},
};
describe
(
'
Environment item
'
,
()
=>
{
let
EnvironmentItem
;
beforeEach
(()
=>
{
EnvironmentItem
=
Vue
.
extend
(
environmentItemComp
);
});
describe
(
'
When item is folder
'
,
()
=>
{
let
mockItem
;
let
component
;
beforeEach
(()
=>
{
mockItem
=
{
name
:
'
review
'
,
folderName
:
'
review
'
,
size
:
3
,
isFolder
:
true
,
environment_path
:
'
url
'
,
log_path
:
'
url
'
,
};
component
=
new
EnvironmentItem
({
propsData
:
{
model
:
mockItem
,
canReadEnvironment
:
true
,
tableData
,
},
}).
$mount
();
});
it
(
'
should render folder icon and name
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.folder-name
'
).
textContent
).
toContain
(
mockItem
.
name
);
expect
(
component
.
$el
.
querySelector
(
'
.folder-icon
'
)).
toBeDefined
();
});
it
(
'
should render the number of children in a badge
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.folder-name .badge
'
).
textContent
).
toContain
(
mockItem
.
size
,
);
});
});
describe
(
'
when item is not folder
'
,
()
=>
{
let
environment
;
let
component
;
beforeEach
(()
=>
{
environment
=
{
name
:
'
production
'
,
size
:
1
,
state
:
'
stopped
'
,
external_url
:
'
http://external.com
'
,
environment_type
:
null
,
last_deployment
:
{
id
:
66
,
iid
:
6
,
sha
:
'
500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
ref
:
{
name
:
'
master
'
,
ref_url
:
'
root/ci-folders/tree/master
'
,
},
tag
:
true
,
'
last?
'
:
true
,
user
:
{
name
:
'
Administrator
'
,
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit
:
{
id
:
'
500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
short_id
:
'
500aabcb
'
,
title
:
'
Update .gitlab-ci.yml
'
,
author_name
:
'
Administrator
'
,
author_email
:
'
admin@example.com
'
,
created_at
:
'
2016-11-07T18:28:13.000+00:00
'
,
message
:
'
Update .gitlab-ci.yml
'
,
author
:
{
name
:
'
Administrator
'
,
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit_path
:
'
/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
},
deployable
:
{
id
:
1279
,
name
:
'
deploy
'
,
build_path
:
'
/root/ci-folders/builds/1279
'
,
retry_path
:
'
/root/ci-folders/builds/1279/retry
'
,
created_at
:
'
2016-11-29T18:11:58.430Z
'
,
updated_at
:
'
2016-11-29T18:11:58.430Z
'
,
},
manual_actions
:
[
{
name
:
'
action
'
,
play_path
:
'
/play
'
,
},
],
deployed_at
:
'
2016-11-29T18:11:58.430Z
'
,
},
has_stop_action
:
true
,
environment_path
:
'
root/ci-folders/environments/31
'
,
log_path
:
'
root/ci-folders/environments/31/logs
'
,
created_at
:
'
2016-11-07T11:11:16.525Z
'
,
updated_at
:
'
2016-11-10T15:55:58.778Z
'
,
};
component
=
new
EnvironmentItem
({
propsData
:
{
model
:
environment
,
canReadEnvironment
:
true
,
tableData
,
},
}).
$mount
();
});
it
(
'
should render environment name
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
).
toContain
(
environment
.
name
,
);
});
describe
(
'
With deployment
'
,
()
=>
{
it
(
'
should render deployment internal id
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
).
toContain
(
environment
.
last_deployment
.
iid
,
);
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
).
toContain
(
'
#
'
);
});
it
(
'
should render last deployment date
'
,
()
=>
{
const
formatedDate
=
format
(
environment
.
last_deployment
.
deployed_at
);
expect
(
component
.
$el
.
querySelector
(
'
.environment-created-date-timeago
'
).
textContent
,
).
toContain
(
formatedDate
);
});
describe
(
'
With user information
'
,
()
=>
{
it
(
'
should render user avatar with link to profile
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-deploy-user-container
'
).
getAttribute
(
'
href
'
),
).
toEqual
(
environment
.
last_deployment
.
user
.
web_url
);
});
});
describe
(
'
With build url
'
,
()
=>
{
it
(
'
should link to build url provided
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
it
(
'
should render deployable name and id
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
});
describe
(
'
With commit information
'
,
()
=>
{
it
(
'
should render commit component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-commit-component
'
)).
toBeDefined
();
});
});
});
describe
(
'
With manual actions
'
,
()
=>
{
it
(
'
should render actions component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-manual-actions-container
'
)).
toBeDefined
();
});
});
describe
(
'
With external URL
'
,
()
=>
{
it
(
'
should render external url component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-external-url-container
'
)).
toBeDefined
();
});
});
describe
(
'
With stop action
'
,
()
=>
{
it
(
'
should render stop action component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-stop-component-container
'
)).
toBeDefined
();
});
});
describe
(
'
With retry action
'
,
()
=>
{
it
(
'
should render rollback component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-rollback-component-container
'
)).
toBeDefined
();
});
});
});
});
spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js
View file @
27d91a62
...
...
@@ -35,4 +35,8 @@ describe('Linked Pipelines Column', () => {
expect
(
linkedPipelineElements
.
length
).
toBe
(
props
.
linkedPipelines
.
length
);
});
it
(
'
renders cross project triangle when column is upstream
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.cross-project-triangle
'
)).
toBeDefined
();
});
});
spec/lib/banzai/reference_parser/mentioned_
users_by_
group_parser_spec.rb
→
spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb
View file @
27d91a62
...
...
@@ -2,7 +2,7 @@
require
'spec_helper'
describe
Banzai
::
ReferenceParser
::
Mentioned
UsersBy
GroupParser
do
describe
Banzai
::
ReferenceParser
::
MentionedGroupParser
do
include
ReferenceParserHelpers
let
(
:group
)
{
create
(
:group
,
:private
)
}
...
...
spec/lib/banzai/reference_parser/mentioned_
users_by_
project_parser_spec.rb
→
spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb
View file @
27d91a62
...
...
@@ -2,7 +2,7 @@
require
'spec_helper'
describe
Banzai
::
ReferenceParser
::
Mentioned
UsersBy
ProjectParser
do
describe
Banzai
::
ReferenceParser
::
MentionedProjectParser
do
include
ReferenceParserHelpers
let
(
:group
)
{
create
(
:group
,
:private
)
}
...
...
spec/lib/gitlab/ci/config/entry/default_spec.rb
View file @
27d91a62
...
...
@@ -27,7 +27,7 @@ describe Gitlab::Ci::Config::Entry::Default do
expect
(
described_class
.
nodes
.
keys
)
.
to
match_array
(
%i[before_script image services
after_script cache interruptible
timeout retry]
)
timeout retry
tags
]
)
end
end
end
...
...
spec/lib/gitlab/ci/config/entry/job_spec.rb
View file @
27d91a62
...
...
@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let
(
:result
)
do
%i[before_script script stage type after_script cache
image services only except rules needs variables artifacts
environment coverage retry interruptible timeout]
environment coverage retry interruptible timeout
tags
]
end
it
{
is_expected
.
to
match_array
result
}
...
...
spec/lib/gitlab/ci/yaml_processor_spec.rb
View file @
27d91a62
...
...
@@ -1849,7 +1849,7 @@ module Gitlab
config
=
YAML
.
dump
({
rspec:
{
script:
"test"
,
tags:
"mysql"
}
})
expect
do
Gitlab
::
Ci
::
YamlProcessor
.
new
(
config
)
end
.
to
raise_error
(
Gitlab
::
Ci
::
YamlProcessor
::
ValidationError
,
"jobs:rspec
tags
should be an array of strings"
)
end
.
to
raise_error
(
Gitlab
::
Ci
::
YamlProcessor
::
ValidationError
,
"jobs:rspec
:tags config
should be an array of strings"
)
end
it
"returns errors if before_script parameter is invalid"
do
...
...
@@ -2197,7 +2197,7 @@ module Gitlab
context
"when the tags parameter is invalid"
do
let
(
:content
)
{
YAML
.
dump
({
rspec:
{
script:
"test"
,
tags:
"mysql"
}
})
}
it
{
is_expected
.
to
eq
"jobs:rspec
tags
should be an array of strings"
}
it
{
is_expected
.
to
eq
"jobs:rspec
:tags config
should be an array of strings"
}
end
context
"when YAML content is empty"
do
...
...
spec/lib/gitlab/import_export/all_models.yml
View file @
27d91a62
...
...
@@ -34,6 +34,7 @@ issues:
-
zoom_meetings
-
vulnerability_links
-
related_vulnerabilities
-
user_mentions
events
:
-
author
-
project
...
...
@@ -82,6 +83,7 @@ snippets:
-
notes
-
award_emoji
-
user_agent_detail
-
user_mentions
releases
:
-
author
-
project
...
...
@@ -142,6 +144,7 @@ merge_requests:
-
description_versions
-
deployment_merge_requests
-
deployments
-
user_mentions
external_pull_requests
:
-
project
merge_request_diff
:
...
...
@@ -539,6 +542,7 @@ design: &design
-
actions
-
versions
-
notes
-
user_mentions
designs
:
*design
actions
:
-
design
...
...
spec/models/broadcast_message_spec.rb
View file @
27d91a62
...
...
@@ -20,65 +20,71 @@ describe BroadcastMessage do
it
{
is_expected
.
to
allow_value
(
triplet
).
for
(
:font
)
}
it
{
is_expected
.
to
allow_value
(
hex
).
for
(
:font
)
}
it
{
is_expected
.
not_to
allow_value
(
'000'
).
for
(
:font
)
}
it
{
is_expected
.
to
allow_value
(
1
).
for
(
:broadcast_type
)
}
it
{
is_expected
.
not_to
allow_value
(
nil
).
for
(
:broadcast_type
)
}
end
describe
'.current'
,
:use_clean_rails_memory_store_caching
do
shared_examples
'time constrainted'
do
|
broadcast_type
|
it
'returns message if time match'
do
message
=
create
(
:broadcast_message
)
message
=
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
).
to
include
(
message
)
expect
(
subject
.
call
).
to
include
(
message
)
end
it
'returns multiple messages if time match'
do
message1
=
create
(
:broadcast_message
)
message2
=
create
(
:broadcast_message
)
message1
=
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
message2
=
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
).
to
contain_exactly
(
message1
,
message2
)
expect
(
subject
.
call
).
to
contain_exactly
(
message1
,
message2
)
end
it
'returns empty list if time not come'
do
create
(
:broadcast_message
,
:future
)
create
(
:broadcast_message
,
:future
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
).
to
be_empty
expect
(
subject
.
call
).
to
be_empty
end
it
'returns empty list if time has passed'
do
create
(
:broadcast_message
,
:expired
)
create
(
:broadcast_message
,
:expired
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
).
to
be_empty
expect
(
subject
.
call
).
to
be_empty
end
end
shared_examples
'message cache'
do
|
broadcast_type
|
it
'caches the output of the query for two weeks'
do
create
(
:broadcast_message
)
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
expect
(
described_class
).
to
receive
(
:current_and_future_messages
).
and_call_original
.
twice
described_class
.
current
subject
.
call
Timecop
.
travel
(
3
.
weeks
)
do
described_class
.
current
subject
.
call
end
end
it
'does not create new records'
do
create
(
:broadcast_message
)
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
expect
{
described_class
.
current
}.
not_to
change
{
described_class
.
count
}
expect
{
subject
.
call
}.
not_to
change
{
described_class
.
count
}
end
it
'includes messages that need to be displayed in the future'
do
create
(
:broadcast_message
)
create
(
:broadcast_message
,
broadcast_type:
broadcast_type
)
future
=
create
(
:broadcast_message
,
starts_at:
Time
.
now
+
10
.
minutes
,
ends_at:
Time
.
now
+
20
.
minutes
ends_at:
Time
.
now
+
20
.
minutes
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
.
length
).
to
eq
(
1
)
expect
(
subject
.
call
.
length
).
to
eq
(
1
)
Timecop
.
travel
(
future
.
starts_at
)
do
expect
(
described_class
.
current
.
length
).
to
eq
(
2
)
expect
(
subject
.
call
.
length
).
to
eq
(
2
)
end
end
...
...
@@ -86,43 +92,90 @@ describe BroadcastMessage do
create
(
:broadcast_message
,
:future
)
expect
(
Rails
.
cache
).
not_to
receive
(
:delete
).
with
(
described_class
::
CACHE_KEY
)
expect
(
described_class
.
current
.
length
).
to
eq
(
0
)
expect
(
subject
.
call
.
length
).
to
eq
(
0
)
end
end
shared_examples
"matches with current path"
do
|
broadcast_type
|
it
'returns message if it matches the target path'
do
message
=
create
(
:broadcast_message
,
target_path:
"*/onboarding_completed"
)
message
=
create
(
:broadcast_message
,
target_path:
"*/onboarding_completed"
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
(
'/users/onboarding_completed'
)).
to
include
(
message
)
expect
(
subject
.
call
(
'/users/onboarding_completed'
)).
to
include
(
message
)
end
it
'returns message if part of the target path matches'
do
create
(
:broadcast_message
,
target_path:
"/users/*/issues"
)
create
(
:broadcast_message
,
target_path:
"/users/*/issues"
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
expect
(
subject
.
call
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
end
it
'returns the message for empty target path'
do
create
(
:broadcast_message
,
target_path:
""
)
create
(
:broadcast_message
,
target_path:
""
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
expect
(
subject
.
call
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
end
it
'returns the message if target path is nil'
do
create
(
:broadcast_message
,
target_path:
nil
)
create
(
:broadcast_message
,
target_path:
nil
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
expect
(
subject
.
call
(
'/users/name/issues'
).
length
).
to
eq
(
1
)
end
it
'does not return message if target path does not match'
do
create
(
:broadcast_message
,
target_path:
"/onboarding_completed"
)
create
(
:broadcast_message
,
target_path:
"/onboarding_completed"
,
broadcast_type:
broadcast_type
)
expect
(
described_class
.
current
(
'/welcome'
).
length
).
to
eq
(
0
)
expect
(
subject
.
call
(
'/welcome'
).
length
).
to
eq
(
0
)
end
it
'does not return message if target path does not match when using wildcard'
do
create
(
:broadcast_message
,
target_path:
"/users/*/issues"
)
create
(
:broadcast_message
,
target_path:
"/users/*/issues"
,
broadcast_type:
broadcast_type
)
expect
(
subject
.
call
(
'/group/groupname/issues'
).
length
).
to
eq
(
0
)
end
end
describe
'.current'
,
:use_clean_rails_memory_store_caching
do
subject
{
->
(
path
=
nil
)
{
described_class
.
current
(
path
)
}
}
it_behaves_like
'time constrainted'
,
:banner
it_behaves_like
'message cache'
,
:banner
it_behaves_like
'matches with current path'
,
:banner
it
'returns both types'
do
banner_message
=
create
(
:broadcast_message
,
broadcast_type: :banner
)
notification_message
=
create
(
:broadcast_message
,
broadcast_type: :notification
)
expect
(
subject
.
call
).
to
contain_exactly
(
banner_message
,
notification_message
)
end
end
describe
'.current_banner_messages'
,
:use_clean_rails_memory_store_caching
do
subject
{
->
(
path
=
nil
)
{
described_class
.
current_banner_messages
(
path
)
}
}
it_behaves_like
'time constrainted'
,
:banner
it_behaves_like
'message cache'
,
:banner
it_behaves_like
'matches with current path'
,
:banner
it
'only returns banners'
do
banner_message
=
create
(
:broadcast_message
,
broadcast_type: :banner
)
create
(
:broadcast_message
,
broadcast_type: :notification
)
expect
(
subject
.
call
).
to
contain_exactly
(
banner_message
)
end
end
describe
'.current_notification_messages'
,
:use_clean_rails_memory_store_caching
do
subject
{
->
(
path
=
nil
)
{
described_class
.
current_notification_messages
(
path
)
}
}
it_behaves_like
'time constrainted'
,
:notification
it_behaves_like
'message cache'
,
:notification
it_behaves_like
'matches with current path'
,
:notification
it
'only returns notifications'
do
notification_message
=
create
(
:broadcast_message
,
broadcast_type: :notification
)
create
(
:broadcast_message
,
broadcast_type: :banner
)
expect
(
described_class
.
current
(
'/group/groupname/issues'
).
length
).
to
eq
(
0
)
expect
(
subject
.
call
).
to
contain_exactly
(
notification_message
)
end
end
...
...
@@ -193,6 +246,8 @@ describe BroadcastMessage do
message
=
create
(
:broadcast_message
)
expect
(
Rails
.
cache
).
to
receive
(
:delete
).
with
(
described_class
::
CACHE_KEY
)
expect
(
Rails
.
cache
).
to
receive
(
:delete
).
with
(
described_class
::
BANNER_CACHE_KEY
)
expect
(
Rails
.
cache
).
to
receive
(
:delete
).
with
(
described_class
::
NOTIFICATION_CACHE_KEY
)
message
.
flush_redis_cache
end
...
...
spec/models/concerns/mentionable_spec.rb
View file @
27d91a62
...
...
@@ -166,6 +166,21 @@ describe Issue, "Mentionable" do
create
(
:issue
,
project:
project
,
description:
description
,
author:
author
)
end
end
describe
'#store_mentions!'
do
it_behaves_like
'mentions in description'
,
:issue
it_behaves_like
'mentions in notes'
,
:issue
do
let
(
:note
)
{
create
(
:note_on_issue
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
describe
'load mentions'
do
it_behaves_like
'load mentions from DB'
,
:issue
do
let
(
:note
)
{
create
(
:note_on_issue
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
end
describe
Commit
,
'Mentionable'
do
...
...
@@ -221,4 +236,56 @@ describe Commit, 'Mentionable' do
end
end
end
describe
'#store_mentions!'
do
it_behaves_like
'mentions in notes'
,
:commit
do
let
(
:note
)
{
create
(
:note_on_commit
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
describe
'load mentions'
do
it_behaves_like
'load mentions from DB'
,
:commit
do
let
(
:note
)
{
create
(
:note_on_commit
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
end
describe
MergeRequest
,
'Mentionable'
do
describe
'#store_mentions!'
do
it_behaves_like
'mentions in description'
,
:merge_request
it_behaves_like
'mentions in notes'
,
:merge_request
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
,
target_project:
project
)
}
let
(
:note
)
{
create
(
:note_on_merge_request
,
noteable:
merge_request
,
project:
merge_request
.
project
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
describe
'load mentions'
do
it_behaves_like
'load mentions from DB'
,
:merge_request
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
,
target_project:
project
)
}
let
(
:note
)
{
create
(
:note_on_merge_request
,
noteable:
merge_request
,
project:
merge_request
.
project
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
end
describe
Snippet
,
'Mentionable'
do
describe
'#store_mentions!'
do
it_behaves_like
'mentions in description'
,
:project_snippet
it_behaves_like
'mentions in notes'
,
:project_snippet
do
let
(
:note
)
{
create
(
:note_on_project_snippet
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
describe
'load mentions'
do
it_behaves_like
'load mentions from DB'
,
:project_snippet
do
let
(
:note
)
{
create
(
:note_on_project_snippet
)
}
let
(
:mentionable
)
{
note
.
noteable
}
end
end
end
spec/models/issue_spec.rb
View file @
27d91a62
...
...
@@ -12,6 +12,7 @@ describe Issue do
it
{
is_expected
.
to
belong_to
(
:duplicated_to
).
class_name
(
'Issue'
)
}
it
{
is_expected
.
to
belong_to
(
:closed_by
).
class_name
(
'User'
)
}
it
{
is_expected
.
to
have_many
(
:assignees
)
}
it
{
is_expected
.
to
have_many
(
:user_mentions
).
class_name
(
"IssueUserMention"
)
}
it
{
is_expected
.
to
have_one
(
:sentry_issue
)
}
end
...
...
spec/models/merge_request_spec.rb
View file @
27d91a62
...
...
@@ -17,6 +17,7 @@ describe MergeRequest do
it
{
is_expected
.
to
belong_to
(
:merge_user
).
class_name
(
"User"
)
}
it
{
is_expected
.
to
have_many
(
:assignees
).
through
(
:merge_request_assignees
)
}
it
{
is_expected
.
to
have_many
(
:merge_request_diffs
)
}
it
{
is_expected
.
to
have_many
(
:user_mentions
).
class_name
(
"MergeRequestUserMention"
)
}
context
'for forks'
do
let!
(
:project
)
{
create
(
:project
)
}
...
...
spec/models/snippet_spec.rb
View file @
27d91a62
...
...
@@ -18,6 +18,7 @@ describe Snippet do
it
{
is_expected
.
to
belong_to
(
:project
)
}
it
{
is_expected
.
to
have_many
(
:notes
).
dependent
(
:destroy
)
}
it
{
is_expected
.
to
have_many
(
:award_emoji
).
dependent
(
:destroy
)
}
it
{
is_expected
.
to
have_many
(
:user_mentions
).
class_name
(
"SnippetUserMention"
)
}
end
describe
'validation'
do
...
...
spec/models/user_mentions/commit_user_mention_spec.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
require
'spec_helper'
describe
CommitUserMention
do
describe
'associations'
do
it
{
is_expected
.
to
belong_to
(
:note
)
}
end
it_behaves_like
'has user mentions'
end
spec/models/user_mentions/issue_user_mention_spec.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
require
'spec_helper'
describe
IssueUserMention
do
describe
'associations'
do
it
{
is_expected
.
to
belong_to
(
:issue
)
}
it
{
is_expected
.
to
belong_to
(
:note
)
}
end
it_behaves_like
'has user mentions'
end
spec/models/user_mentions/merge_request_user_mention_spec.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
require
'spec_helper'
describe
MergeRequestUserMention
do
describe
'associations'
do
it
{
is_expected
.
to
belong_to
(
:merge_request
)
}
it
{
is_expected
.
to
belong_to
(
:note
)
}
end
it_behaves_like
'has user mentions'
end
spec/models/user_mentions/snippet_user_mention_spec.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
require
'spec_helper'
describe
SnippetUserMention
do
describe
'associations'
do
it
{
is_expected
.
to
belong_to
(
:snippet
)
}
it
{
is_expected
.
to
belong_to
(
:note
)
}
end
it_behaves_like
'has user mentions'
end
spec/support/shared_examples/mentionable_shared_examples.rb
View file @
27d91a62
...
...
@@ -195,3 +195,153 @@ shared_examples 'an editable mentionable' do
subject
.
create_new_cross_references!
(
author
)
end
end
shared_examples_for
'mentions in description'
do
|
mentionable_type
|
describe
'when store_mentioned_users_to_db feature disabled'
do
before
do
stub_feature_flags
(
store_mentioned_users_to_db:
false
)
mentionable
.
store_mentions!
end
context
'when mentionable description contains mentions'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:mentionable
)
{
create
(
mentionable_type
,
description:
"
#{
user
.
to_reference
}
some description"
)
}
it
'stores no mentions'
do
expect
(
mentionable
.
user_mentions
.
count
).
to
eq
0
end
end
end
describe
'when store_mentioned_users_to_db feature enabled'
do
before
do
stub_feature_flags
(
store_mentioned_users_to_db:
true
)
mentionable
.
store_mentions!
end
context
'when mentionable description has no mentions'
do
let
(
:mentionable
)
{
create
(
mentionable_type
,
description:
"just some description"
)
}
it
'stores no mentions'
do
expect
(
mentionable
.
user_mentions
.
count
).
to
eq
0
end
end
context
'when mentionable description contains mentions'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:mentionable_desc
)
{
"
#{
user
.
to_reference
}
some description
#{
group
.
to_reference
(
full:
true
)
}
and @all"
}
let
(
:mentionable
)
{
create
(
mentionable_type
,
description:
mentionable_desc
)
}
it
'stores mentions'
do
add_member
(
user
)
expect
(
mentionable
.
user_mentions
.
count
).
to
eq
1
expect
(
mentionable
.
referenced_users
).
to
match_array
([
user
])
expect
(
mentionable
.
referenced_projects
(
user
)).
to
match_array
([
mentionable
.
project
].
compact
)
# epic.project is nil, and we want empty []
expect
(
mentionable
.
referenced_groups
(
user
)).
to
match_array
([
group
])
end
end
end
end
shared_examples_for
'mentions in notes'
do
|
mentionable_type
|
context
'when mentionable notes contain mentions'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
)
}
let
(
:note_desc
)
{
"
#{
user
.
to_reference
}
and
#{
group
.
to_reference
(
full:
true
)
}
and @all"
}
let!
(
:mentionable
)
{
note
.
noteable
}
before
do
note
.
update
(
note:
note_desc
)
note
.
store_mentions!
add_member
(
user
)
end
it
'returns all mentionable mentions'
do
expect
(
mentionable
.
user_mentions
.
count
).
to
eq
1
expect
(
mentionable
.
referenced_users
).
to
eq
[
user
]
expect
(
mentionable
.
referenced_projects
(
user
)).
to
eq
[
mentionable
.
project
].
compact
# epic.project is nil, and we want empty []
expect
(
mentionable
.
referenced_groups
(
user
)).
to
eq
[
group
]
end
end
end
shared_examples_for
'load mentions from DB'
do
|
mentionable_type
|
context
'load stored mentions'
do
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:mentioned_user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:note_desc
)
{
"
#{
mentioned_user
.
to_reference
}
and
#{
group
.
to_reference
(
full:
true
)
}
and @all"
}
before
do
note
.
update
(
note:
note_desc
)
note
.
store_mentions!
add_member
(
user
)
end
context
'when stored user mention contains ids of inexistent records'
do
before
do
user_mention
=
note
.
send
(
:model_user_mention
)
mention_ids
=
{
mentioned_users_ids:
user_mention
.
mentioned_users_ids
.
to_a
<<
User
.
maximum
(
:id
).
to_i
.
succ
,
mentioned_projects_ids:
user_mention
.
mentioned_projects_ids
.
to_a
<<
Project
.
maximum
(
:id
).
to_i
.
succ
,
mentioned_groups_ids:
user_mention
.
mentioned_groups_ids
.
to_a
<<
Group
.
maximum
(
:id
).
to_i
.
succ
}
user_mention
.
update
(
mention_ids
)
end
it
'filters out inexistent mentions'
do
expect
(
mentionable
.
referenced_users
).
to
match_array
([
mentioned_user
])
expect
(
mentionable
.
referenced_projects
(
user
)).
to
match_array
([
mentionable
.
project
].
compact
)
# epic.project is nil, and we want empty []
expect
(
mentionable
.
referenced_groups
(
user
)).
to
match_array
([
group
])
end
end
context
'when private projects and groups are mentioned'
do
let
(
:mega_user
)
{
create
(
:user
)
}
let
(
:private_project
)
{
create
(
:project
,
:private
)
}
let
(
:project_member
)
{
create
(
:project_member
,
user:
create
(
:user
),
project:
private_project
)
}
let
(
:private_group
)
{
create
(
:group
,
:private
)
}
let
(
:group_member
)
{
create
(
:group_member
,
user:
create
(
:user
),
group:
private_group
)
}
before
do
user_mention
=
note
.
send
(
:model_user_mention
)
mention_ids
=
{
mentioned_projects_ids:
user_mention
.
mentioned_projects_ids
.
to_a
<<
private_project
.
id
,
mentioned_groups_ids:
user_mention
.
mentioned_groups_ids
.
to_a
<<
private_group
.
id
}
user_mention
.
update
(
mention_ids
)
add_member
(
mega_user
)
private_project
.
add_developer
(
mega_user
)
private_group
.
add_developer
(
mega_user
)
end
context
'when user has no access to some mentions'
do
it
'filters out inaccessible mentions'
do
expect
(
mentionable
.
referenced_projects
(
user
)).
to
match_array
([
mentionable
.
project
].
compact
)
# epic.project is nil, and we want empty []
expect
(
mentionable
.
referenced_groups
(
user
)).
to
match_array
([
group
])
end
end
context
'when user has access to all mentions'
do
it
'returns all mentions'
do
expect
(
mentionable
.
referenced_projects
(
mega_user
)).
to
match_array
([
mentionable
.
project
,
private_project
].
compact
)
# epic.project is nil, and we want empty []
expect
(
mentionable
.
referenced_groups
(
mega_user
)).
to
match_array
([
group
,
private_group
])
end
end
end
end
end
def
add_member
(
user
)
issuable_parent
=
if
mentionable
.
is_a?
(
Epic
)
mentionable
.
group
else
mentionable
.
project
end
issuable_parent
&
.
add_developer
(
user
)
end
spec/support/shared_examples/models/user_mentions_shared_examples.rb
0 → 100644
View file @
27d91a62
# frozen_string_literal: true
require
'spec_helper'
shared_examples_for
'has user mentions'
do
describe
'#has_mentions?'
do
context
'when no mentions'
do
it
'returns false'
do
expect
(
subject
.
mentioned_users_ids
).
to
be
nil
expect
(
subject
.
mentioned_projects_ids
).
to
be
nil
expect
(
subject
.
mentioned_groups_ids
).
to
be
nil
expect
(
subject
.
has_mentions?
).
to
be
false
end
end
context
'when mentioned_users_ids not null'
do
subject
{
described_class
.
new
(
mentioned_users_ids:
[
1
,
2
,
3
])
}
it
'returns true'
do
expect
(
subject
.
has_mentions?
).
to
be
true
end
end
context
'when mentioned projects'
do
subject
{
described_class
.
new
(
mentioned_projects_ids:
[
1
,
2
,
3
])
}
it
'returns true'
do
expect
(
subject
.
has_mentions?
).
to
be
true
end
end
context
'when mentioned groups'
do
subject
{
described_class
.
new
(
mentioned_groups_ids:
[
1
,
2
,
3
])
}
it
'returns true'
do
expect
(
subject
.
has_mentions?
).
to
be
true
end
end
end
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