Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-10 18:07:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-10 18:07:52 +0300
commit27d91a629918e417a9e87825e838209b9ace79c1 (patch)
treee066c3fc84e3011641e662252810cb2c240edb90 /app
parent5e11c9b77cb1b2b77ee29359047b55807afe255d (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue7
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb1
-rw-r--r--app/models/broadcast_message.rb76
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/commit_user_mention.rb5
-rw-r--r--app/models/concerns/mentionable.rb80
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/issue_user_mention.rb6
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/merge_request_user_mention.rb6
-rw-r--r--app/models/note.rb10
-rw-r--r--app/models/snippet.rb3
-rw-r--r--app/models/snippet_user_mention.rb6
-rw-r--r--app/models/user_mention.rb23
-rw-r--r--app/services/create_snippet_service.rb6
-rw-r--r--app/services/issuable_base_service.rb12
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notes/update_service.rb6
-rw-r--r--app/services/update_snippet_service.rb8
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
23 files changed, 238 insertions, 38 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index e29509ce074..429122c8083 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -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),
}"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 6efdde2b17e..998519f9df1 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,5 +1,6 @@
<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',
}"
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index a1afcf5077e..f394e4ab58a 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -473,6 +473,7 @@ table.code {
text-align: right;
width: 50px;
position: relative;
+ white-space: nowrap;
a {
transition: none;
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 63fff821871..06ba916fc55 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -61,6 +61,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
message
starts_at
target_path
+ broadcast_type
))
end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 9c2ae92071d..b3d72ebdcf3 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -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
diff --git a/app/models/commit.rb b/app/models/commit.rb
index c683a9aeb76..01c7991ba2f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -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
diff --git a/app/models/commit_user_mention.rb b/app/models/commit_user_mention.rb
new file mode 100644
index 00000000000..680d20b61cf
--- /dev/null
+++ b/app/models/commit_user_mention.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class CommitUserMention < UserMention
+ belongs_to :note
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 9b6c57261d8..b43b91699ab 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -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')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2f8d6cb0c06..d49f3c28000 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -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
diff --git a/app/models/issue_user_mention.rb b/app/models/issue_user_mention.rb
new file mode 100644
index 00000000000..3eadd580f7f
--- /dev/null
+++ b/app/models/issue_user_mention.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class IssueUserMention < UserMention
+ belongs_to :issue
+ belongs_to :note
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f4dfd9128fa..240c143abba 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -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
diff --git a/app/models/merge_request_user_mention.rb b/app/models/merge_request_user_mention.rb
new file mode 100644
index 00000000000..222d9c1aa8c
--- /dev/null
+++ b/app/models/merge_request_user_mention.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class MergeRequestUserMention < UserMention
+ belongs_to :merge_request
+ belongs_to :note
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index f5f6ecf6336..cfa7ba98081 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -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
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b802ea2fd59..92746d28f05 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -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
diff --git a/app/models/snippet_user_mention.rb b/app/models/snippet_user_mention.rb
new file mode 100644
index 00000000000..87ce77a5787
--- /dev/null
+++ b/app/models/snippet_user_mention.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class SnippetUserMention < UserMention
+ belongs_to :snippet
+ belongs_to :note
+end
diff --git a/app/models/user_mention.rb b/app/models/user_mention.rb
new file mode 100644
index 00000000000..a85c6168cea
--- /dev/null
+++ b/app/models/user_mention.rb
@@ -0,0 +1,23 @@
+# 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
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 0aa76df35ba..eacea7d94c7 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -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
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index bb65a8f402d..6cb84458d9b 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -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)
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 9e6cbfa06fe..3468341a0f2 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -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
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 573be8fbe8b..15c556498ec 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -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
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
index a294812ef9e..ac7f8e9b1f5 100644
--- a/app/services/update_snippet_service.rb
+++ b/app/services/update_snippet_service.rb
@@ -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
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 84198489e41..255a62d0d06 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -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")
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 67dad9b7a75..5b9af0267cc 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -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