...@@ -245,7 +245,7 @@ export default { ...@@ -245,7 +245,7 @@ export default {
discussion => discussion =>
!discussion.individual_note && !discussion.individual_note &&
discussion.resolvable && discussion.resolvable &&
discussion.notes.some(note => !note.resolved), discussion.notes.some(note => note.resolvable && !note.resolved),
).length; ).length;
state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1; state.hasUnresolvedDiscussions = state.unresolvedDiscussionsCount > 1;
}, },
... ...
......
...@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController
end end
def gcp_cluster def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_provider_gcp cluster.build_provider_gcp
end.present(current_user: current_user) @gcp_cluster = cluster.present(current_user: current_user)
end end
def user_cluster def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster| cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_platform_kubernetes cluster.build_platform_kubernetes
end.present(current_user: current_user) @user_cluster = cluster.present(current_user: current_user)
end end
def validate_gcp_token def validate_gcp_token
... ...
......
...@@ -128,7 +128,7 @@ class Notify < BaseMailer ...@@ -128,7 +128,7 @@ class Notify < BaseMailer
address.display_name = reply_display_name(model) address.display_name = reply_display_name(model)
end end
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>"
headers['References'] ||= [] headers['References'] ||= []
headers['References'].unshift(fallback_reply_message_id) headers['References'].unshift(fallback_reply_message_id)
...@@ -166,7 +166,7 @@ class Notify < BaseMailer ...@@ -166,7 +166,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model) headers['In-Reply-To'] = message_id(model)
headers['References'] = [message_id(model)] headers['References'] = [message_id(model)]
headers[:subject]&.prepend('Re: ') headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers) mail_thread(model, headers)
end end
...@@ -178,7 +178,7 @@ class Notify < BaseMailer ...@@ -178,7 +178,7 @@ class Notify < BaseMailer
headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion? headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion?
headers[:subject]&.prepend('Re: ') headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers) mail_thread(model, headers)
end end
... ...
......
...@@ -16,14 +16,20 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -16,14 +16,20 @@ class BroadcastMessage < ActiveRecord::Base
default_value_for :color, '#E75E40' default_value_for :color, '#E75E40'
default_value_for :font, '#FFFFFF' default_value_for :font, '#FFFFFF'
CACHE_KEY = 'broadcast_message_current'.freeze CACHE_KEY = 'broadcast_message_current_json'.freeze
LEGACY_CACHE_KEY = 'broadcast_message_current'.freeze
after_commit :flush_redis_cache after_commit :flush_redis_cache
def self.current def self.current
messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) { current_and_future_messages.to_a } raw_messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) do
remove_legacy_cache_key
current_and_future_messages.to_json
end
messages = decode_messages(raw_messages)
return messages if messages.empty? return [] unless messages&.present?
now_or_future = messages.select(&:now_or_future?) now_or_future = messages.select(&:now_or_future?)
...@@ -34,6 +40,27 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -34,6 +40,27 @@ class BroadcastMessage < ActiveRecord::Base
now_or_future.select(&:now?) now_or_future.select(&:now?)
end end
def self.decode_messages(raw_messages)
return unless raw_messages&.present?
message_list = ActiveSupport::JSON.decode(raw_messages)
return unless message_list.is_a?(Array)
valid_attr = BroadcastMessage.attribute_names
message_list.map do |raw|
BroadcastMessage.new(raw) if valid_cache_entry?(raw, valid_attr)
end.compact
rescue ActiveSupport::JSON.parse_error
end
def self.valid_cache_entry?(raw, valid_attr)
return false unless raw.is_a?(Hash)
(raw.keys - valid_attr).empty?
end
def self.current_and_future_messages def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).order_id_asc where('ends_at > :now', now: Time.zone.now).order_id_asc
end end
...@@ -42,6 +69,14 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -42,6 +69,14 @@ class BroadcastMessage < ActiveRecord::Base
nil nil
end end
# This can be removed in GitLab 12.0+
# The old cache key had an indefinite lifetime, and in an HA
# environment a one-shot migration would not work because the cache
# would be repopulated by a node that has not been upgraded.
def self.remove_legacy_cache_key
Rails.cache.delete(LEGACY_CACHE_KEY)
end
def active? def active?
started? && !ended? started? && !ended?
end end
...@@ -68,5 +103,6 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -68,5 +103,6 @@ class BroadcastMessage < ActiveRecord::Base
def flush_redis_cache def flush_redis_cache
Rails.cache.delete(CACHE_KEY) Rails.cache.delete(CACHE_KEY)
self.class.remove_legacy_cache_key
end end
end end
...@@ -978,6 +978,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -978,6 +978,7 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state? def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds? return true unless project.only_allow_merge_if_pipeline_succeeds?
return true unless head_pipeline
actual_head_pipeline&.success? || actual_head_pipeline&.skipped? actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end end
... ...
......
# frozen_string_literal: true
module Clusters
class BuildService
def initialize(subject)
@subject = subject
end
def execute
::Clusters::Cluster.new.tap do |cluster|
case @subject
when ::Project
cluster.cluster_type = :project_type
when ::Group
cluster.cluster_type = :group_type
else
raise NotImplementedError
end
end
end
end
end
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
%span.append-right-4.js-clone-dropdown-label %span.append-right-4.js-clone-dropdown-label
= _('Clone') = _('Clone')
= sprite_icon("arrow-down", css_class: "icon") = sprite_icon("arrow-down", css_class: "icon")
%form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown %form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
%li.pb-2 %li.pb-2
%label.label-bold %label.label-bold
= _('Clone with SSH') = _('Clone with SSH')
.input-group .input-group
= text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append .input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo' = render_if_exists 'projects/buttons/geo'
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%label.label-bold %label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group .input-group
= text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append .input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") = clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo' = render_if_exists 'projects/buttons/geo'
... ...
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
.prepend-top-20 .prepend-top-20
%nav.project-buttons %nav.project-buttons
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.qa-quick-actions
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
.nav-links.scrolling-tabs.quick-links .nav-links.scrolling-tabs.quick-links
... ...
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
- if can_collaborate || can_create_mr_from_fork - if can_collaborate || can_create_mr_from_fork
%li.breadcrumb-item %li.breadcrumb-item
%a.btn.add-to-tree{ addtotree_toggle_attributes } %a.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes }
= sprite_icon('plus', size: 16, css_class: 'float-left') = sprite_icon('plus', size: 16, css_class: 'float-left')
= sprite_icon('arrow-down', size: 16, css_class: 'float-left') = sprite_icon('arrow-down', size: 16, css_class: 'float-left')
- if on_top_of_branch? - if on_top_of_branch?
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%li.dropdown-header %li.dropdown-header
#{ _('This directory') } #{ _('This directory') }
%li %li
= link_to project_new_blob_path(@project, @id) do = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
#{ _('New file') } #{ _('New file') }
%li %li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
... ...
......
---
title: Strictly require a pipeline to merge.
merge_request: 22911
author:
type: changed
---
title: Fix a frozen string error in app/mailers/notify.rb
merge_request: 23683
author:
type: fixed
---
title: Fix a frozen string error in lib/gitlab/utils.rb
merge_request: 23690
author:
type: fixed
---
title: Fix MR resolved discussion counts being too low
merge_request: 23710
author:
type: fixed
---
title: Fix a potential frozen string error in app/mailers/notify.rb
merge_request: 23728
author:
type: fixed
---
title: Only allow strings in URL::Sanitizer.valid?
merge_request: 23675
author:
type: fixed
---
title: Avoid caching BroadcastMessage as an ActiveRecord object
merge_request: 23662
author:
type: fixed
doc/user/project/issues/img/similar_issues.png

66.6 KiB

...@@ -155,3 +155,7 @@ Read through the [API documentation](../../../api/issues.md). ...@@ -155,3 +155,7 @@ Read through the [API documentation](../../../api/issues.md).
### Bulk editing issues ### Bulk editing issues
Find out about [bulk editing issues](../../project/bulk_editing.md). Find out about [bulk editing issues](../../project/bulk_editing.md).
### Similar issues
Find out about [similar issues](similar_issues.md).
# Similar issues
> [Introduced][ce-22866] in GitLab 11.6.
Similar issues suggests issues that are similar when new issues are being created.
This features requires [GraphQL] to be enabled.
![Similar issues](img/similar_issues.png)
You can see the similar issues when typing in the title in the new issue form.
This searches both titles and descriptions across all issues the user has access
to in the current project. It then displays the first 5 issues sorted by most
recently updated.
[GraphQL]: ../../../api/graphql/index.md
[ce-22866]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
}.freeze }.freeze
def validate! def validate!
return if deletion? || newrev.nil?
return unless should_run_diff_validations? return unless should_run_diff_validations?
return if commits.empty? return if commits.empty?
return unless uses_raw_delta_validations? return unless uses_raw_delta_validations?
...@@ -28,7 +29,7 @@ module Gitlab ...@@ -28,7 +29,7 @@ module Gitlab
private private
def should_run_diff_validations? def should_run_diff_validations?
newrev && oldrev && !deletion? && validate_lfs_file_locks? validate_lfs_file_locks?
end end
def validate_lfs_file_locks? def validate_lfs_file_locks?
... ...
......