Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
56a177ed56
commit
bc2f7ab125
|
@ -34,10 +34,6 @@ brakeman-sast:
|
|||
rules: !reference [".reports:rules:brakeman-sast", rules]
|
||||
allow_failure: true
|
||||
|
||||
nodejs-scan-sast:
|
||||
rules: !reference [".reports:rules:nodejs-scan-sast", rules]
|
||||
allow_failure: true
|
||||
|
||||
semgrep-sast:
|
||||
rules: !reference [".reports:rules:semgrep-sast", rules]
|
||||
allow_failure: true
|
||||
|
|
|
@ -1265,15 +1265,6 @@
|
|||
- '**/*.rb'
|
||||
- '**/Gemfile'
|
||||
|
||||
.reports:rules:nodejs-scan-sast:
|
||||
rules:
|
||||
- if: $SAST_DISABLED
|
||||
when: never
|
||||
- if: $SAST_EXCLUDED_ANALYZERS =~ /nodejs-scan/
|
||||
when: never
|
||||
- changes:
|
||||
- '**/package.json'
|
||||
|
||||
.reports:rules:gosec-sast:
|
||||
rules:
|
||||
- if: $SAST_DISABLED
|
||||
|
|
|
@ -1 +1 @@
|
|||
14.3.3
|
||||
14.4.0
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import $ from 'jquery';
|
||||
import GLForm from '~/gl_form';
|
||||
import initFilePickers from '~/file_pickers';
|
||||
import ZenMode from '~/zen_mode';
|
||||
|
||||
new GLForm($('.js-project-topic-form')); // eslint-disable-line no-new
|
||||
initFilePickers();
|
||||
new ZenMode(); // eslint-disable-line no-new
|
|
@ -0,0 +1,8 @@
|
|||
import $ from 'jquery';
|
||||
import GLForm from '~/gl_form';
|
||||
import initFilePickers from '~/file_pickers';
|
||||
import ZenMode from '~/zen_mode';
|
||||
|
||||
new GLForm($('.js-project-topic-form')); // eslint-disable-line no-new
|
||||
initFilePickers();
|
||||
new ZenMode(); // eslint-disable-line no-new
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Topics::AvatarsController < Admin::ApplicationController
|
||||
feature_category :projects
|
||||
|
||||
def destroy
|
||||
@topic = Projects::Topic.find(params[:topic_id])
|
||||
|
||||
@topic.remove_avatar!
|
||||
@topic.save
|
||||
|
||||
redirect_to edit_admin_topic_path(@topic), status: :found
|
||||
end
|
||||
end
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::TopicsController < Admin::ApplicationController
|
||||
include SendFileUpload
|
||||
include PreviewMarkdown
|
||||
|
||||
before_action :topic, only: [:edit, :update]
|
||||
|
||||
feature_category :projects
|
||||
|
||||
def index
|
||||
@topics = Projects::TopicsFinder.new(params: params.permit(:search)).execute.page(params[:page]).without_count
|
||||
end
|
||||
|
||||
def new
|
||||
@topic = Projects::Topic.new
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@topic = Projects::Topic.new(topic_params)
|
||||
|
||||
if @topic.save
|
||||
redirect_to edit_admin_topic_path(@topic), notice: _('Topic %{topic_name} was successfully created.') % { topic_name: @topic.name }
|
||||
else
|
||||
render "new"
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @topic.update(topic_params)
|
||||
redirect_to edit_admin_topic_path(@topic), notice: _('Topic was successfully updated.')
|
||||
else
|
||||
render "edit"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def topic
|
||||
@topic ||= Projects::Topic.find(params[:id])
|
||||
end
|
||||
|
||||
def topic_params
|
||||
params.require(:projects_topic).permit(allowed_topic_params)
|
||||
end
|
||||
|
||||
def allowed_topic_params
|
||||
[
|
||||
:avatar,
|
||||
:description,
|
||||
:name
|
||||
]
|
||||
end
|
||||
end
|
|
@ -13,6 +13,7 @@ class UploadsController < ApplicationController
|
|||
"group" => Group,
|
||||
"appearance" => Appearance,
|
||||
"personal_snippet" => PersonalSnippet,
|
||||
"projects/topic" => Projects::Topic,
|
||||
nil => PersonalSnippet
|
||||
}.freeze
|
||||
|
||||
|
@ -54,6 +55,8 @@ class UploadsController < ApplicationController
|
|||
!secret? || can?(current_user, :update_user, model)
|
||||
when Appearance
|
||||
true
|
||||
when Projects::Topic
|
||||
true
|
||||
else
|
||||
permission = "read_#{model.class.underscore}".to_sym
|
||||
|
||||
|
@ -85,7 +88,7 @@ class UploadsController < ApplicationController
|
|||
|
||||
def cache_settings
|
||||
case model
|
||||
when User, Appearance
|
||||
when User, Appearance, Projects::Topic
|
||||
[5.minutes, { public: true, must_revalidate: false }]
|
||||
when Project, Group
|
||||
[5.minutes, { private: true, must_revalidate: true }]
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Used to filter project topics by a set of params
|
||||
#
|
||||
# Arguments:
|
||||
# params:
|
||||
# search: string
|
||||
module Projects
|
||||
class TopicsFinder
|
||||
def initialize(params: {})
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
topics = Projects::Topic.order_by_total_projects_count
|
||||
by_search(topics)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :params
|
||||
|
||||
def by_search(topics)
|
||||
return topics unless params[:search].present?
|
||||
|
||||
topics.search(params[:search]).reorder_by_similarity(params[:search])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,10 @@ module AvatarsHelper
|
|||
source_icon(group, options)
|
||||
end
|
||||
|
||||
def topic_icon(topic, options = {})
|
||||
source_icon(topic, options)
|
||||
end
|
||||
|
||||
# Takes both user and email and returns the avatar_icon by
|
||||
# user (preferred) or email.
|
||||
def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
|
||||
|
|
|
@ -7,7 +7,6 @@ module Ci
|
|||
self.limit_name = 'ci_registered_group_runners'
|
||||
self.limit_scope = :group
|
||||
self.limit_relation = :recent_runners
|
||||
self.limit_feature_flag = :ci_runner_limits
|
||||
self.limit_feature_flag_for_override = :ci_runner_limits_override
|
||||
|
||||
belongs_to :runner, inverse_of: :runner_namespaces
|
||||
|
|
|
@ -7,7 +7,6 @@ module Ci
|
|||
self.limit_name = 'ci_registered_project_runners'
|
||||
self.limit_scope = :project
|
||||
self.limit_relation = :recent_runners
|
||||
self.limit_feature_flag = :ci_runner_limits
|
||||
self.limit_feature_flag_for_override = :ci_runner_limits_override
|
||||
|
||||
belongs_to :runner, inverse_of: :runner_projects
|
||||
|
|
|
@ -18,6 +18,7 @@ module Avatarable
|
|||
prepend ShadowMethods
|
||||
include ObjectStorage::BackgroundMove
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include ApplicationHelper
|
||||
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validates :avatar, file_size: { maximum: MAXIMUM_FILE_SIZE }, if: :avatar_changed?
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
module Projects
|
||||
class ProjectTopic < ApplicationRecord
|
||||
belongs_to :project
|
||||
belongs_to :topic
|
||||
belongs_to :topic, counter_cache: :total_projects_count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'carrierwave/orm/activerecord'
|
||||
|
||||
module Projects
|
||||
class Topic < ApplicationRecord
|
||||
include Avatarable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
||||
validates :description, length: { maximum: 1024 }
|
||||
|
||||
has_many :project_topics, class_name: 'Projects::ProjectTopic'
|
||||
has_many :projects, through: :project_topics
|
||||
|
||||
scope :order_by_total_projects_count, -> { order(total_projects_count: :desc).order(id: :asc) }
|
||||
scope :reorder_by_similarity, -> (search) do
|
||||
order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
|
||||
{ column: arel_table['name'] }
|
||||
])
|
||||
reorder(order_expression.desc, arel_table['total_projects_count'].desc, arel_table['id'])
|
||||
end
|
||||
|
||||
class << self
|
||||
def search(query)
|
||||
fuzzy_search(query, [:name])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
= gitlab_ui_form_for @topic, url: url, html: { multipart: true, class: 'js-project-topic-form gl-show-field-errors common-note-form js-quick-submit js-requires-input' }, authenticity_token: true do |f|
|
||||
= form_errors(@topic)
|
||||
|
||||
.form-group
|
||||
= f.label :name do
|
||||
= _("Topic name")
|
||||
= f.text_field :name, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
|
||||
required: true,
|
||||
title: _('Please fill in a name for your topic.'),
|
||||
autofocus: true
|
||||
|
||||
.form-group
|
||||
= f.label :description, _("Description")
|
||||
= render layout: 'shared/md_preview', locals: { url: preview_markdown_admin_topics_path, referenced_users: false } do
|
||||
= render 'shared/zen', f: f, attr: :description,
|
||||
classes: 'note-textarea',
|
||||
placeholder: _('Write a description…'),
|
||||
supports_quick_actions: false,
|
||||
supports_autocomplete: false,
|
||||
qa_selector: 'topic_form_description'
|
||||
= render 'shared/notes/hints', supports_file_upload: false
|
||||
|
||||
.form-group.gl-mt-3.gl-mb-3
|
||||
= f.label :avatar, _('Topic avatar'), class: 'gl-display-block'
|
||||
- if @topic.avatar?
|
||||
.avatar-container.rect-avatar.s90
|
||||
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s90')
|
||||
= render 'shared/choose_avatar_button', f: f
|
||||
- if @topic.avatar?
|
||||
= link_to _('Remove avatar'), admin_topic_avatar_path(@topic), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'gl-button btn btn-danger-secondary gl-mt-2'
|
||||
|
||||
- if @topic.new_record?
|
||||
.form-actions
|
||||
= f.submit _('Create topic'), class: "gl-button btn btn-confirm"
|
||||
= link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-default btn-cancel"
|
||||
|
||||
- else
|
||||
.form-actions
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
|
||||
= link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-cancel"
|
|
@ -0,0 +1,17 @@
|
|||
- topic = local_assigns.fetch(:topic)
|
||||
|
||||
%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'topic_row_content' } }
|
||||
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
|
||||
= topic_icon(topic, class: "avatar s40")
|
||||
|
||||
.gl-min-w-0.gl-flex-grow-1
|
||||
.title
|
||||
= topic.name
|
||||
|
||||
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
|
||||
%span.gl-ml-5.has-tooltip{ title: n_('%d project', '%d projects', topic.total_projects_count) % topic.total_projects_count }
|
||||
= sprite_icon('bookmark', css_class: 'gl-vertical-align-text-bottom')
|
||||
= number_with_delimiter(topic.total_projects_count)
|
||||
|
||||
.controls.gl-flex-shrink-0.gl-ml-5
|
||||
= link_to _('Edit'), edit_admin_topic_path(topic), id: "edit_#{dom_id(topic)}", class: 'btn gl-button btn-default'
|
|
@ -0,0 +1,4 @@
|
|||
- page_title _("Edit"), @topic.name, _("Topics")
|
||||
%h3.page-title= _('Edit topic: %{topic_name}') % { topic_name: @topic.name }
|
||||
%hr
|
||||
= render 'form', url: admin_topic_path(@topic)
|
|
@ -0,0 +1,19 @@
|
|||
- page_title _("Topics")
|
||||
|
||||
= form_tag admin_topics_path, method: :get do |f|
|
||||
.gl-py-3.gl-display-flex.gl-flex-direction-column-reverse.gl-md-flex-direction-row.gl-border-b-solid.gl-border-gray-100.gl-border-b-1
|
||||
.gl-flex-grow-1.gl-mt-3.gl-md-mt-0
|
||||
.inline.gl-w-full.gl-md-w-auto
|
||||
- search = params.fetch(:search, nil)
|
||||
.search-field-holder
|
||||
= search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name'), data: { qa_selector: 'topic_search_field' }
|
||||
= sprite_icon('search', css_class: 'search-icon')
|
||||
.nav-controls
|
||||
= link_to new_admin_topic_path, class: "gl-button btn btn-confirm gl-w-full gl-md-w-auto" do
|
||||
= _('New topic')
|
||||
%ul.content-list
|
||||
= render partial: 'topic', collection: @topics
|
||||
|
||||
= paginate_collection @topics
|
||||
- if @topics.empty?
|
||||
= render 'shared/empty_states/topics'
|
|
@ -0,0 +1,4 @@
|
|||
- page_title _("New topic")
|
||||
%h3.page-title= _('New topic')
|
||||
%hr
|
||||
= render 'form', url: admin_topics_path(@topic)
|
|
@ -7,7 +7,7 @@
|
|||
%span.sidebar-context-title
|
||||
= _('Admin Area')
|
||||
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
|
||||
= nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts), html_options: {class: 'home'}) do
|
||||
= nav_link(controller: %w(dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts), html_options: {class: 'home'}) do
|
||||
= link_to admin_root_path, class: 'has-sub-items' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('overview')
|
||||
|
@ -35,6 +35,10 @@
|
|||
= link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do
|
||||
%span
|
||||
= _('Groups')
|
||||
= nav_link(controller: [:admin, 'admin/topics']) do
|
||||
= link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do
|
||||
%span
|
||||
= _('Topics')
|
||||
= nav_link path: 'jobs#index' do
|
||||
= link_to admin_jobs_path, title: _('Jobs') do
|
||||
%span
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- @no_breadcrumb_container = true
|
||||
- @no_container = true
|
||||
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
|
||||
- @content_class = "issue-boards-content js-focus-mode-board"
|
||||
- @content_class = "js-focus-mode-board"
|
||||
- is_epic_board = board.to_type == "EpicBoard"
|
||||
- if is_epic_board
|
||||
- breadcrumb_title _("Epic Boards")
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.row.empty-state
|
||||
.col-12
|
||||
.svg-content
|
||||
= image_tag 'illustrations/labels.svg', data: { qa_selector: 'svg_content' }
|
||||
.text-content.gl-text-center.gl-pt-0!
|
||||
%h4= _('There are no topics to show.')
|
||||
%p= _('Add topics to projects to help users find them.')
|
|
@ -1,4 +1,5 @@
|
|||
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
|
||||
- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
|
||||
.comment-toolbar.clearfix
|
||||
.toolbar-text
|
||||
= link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank'
|
||||
|
@ -10,33 +11,34 @@
|
|||
is
|
||||
supported
|
||||
|
||||
%span.uploading-container
|
||||
%span.uploading-progress-container.hide
|
||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
%span.attaching-file-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%span.uploading-progress 0%
|
||||
= loading_icon(css_class: 'align-text-bottom gl-mr-2')
|
||||
|
||||
%span.uploading-error-container.hide
|
||||
%span.uploading-error-icon
|
||||
- if supports_file_upload
|
||||
%span.uploading-container
|
||||
%span.uploading-progress-container.hide
|
||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
%span.uploading-error-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
|
||||
%span.gl-button-text
|
||||
= _("Try again")
|
||||
= _("or")
|
||||
%button.btn.gl-button.btn-link.attach-new-file.markdown-selector.gl-vertical-align-baseline
|
||||
%span.gl-button-text
|
||||
= _("attach a new file")
|
||||
= _(".")
|
||||
%span.attaching-file-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%span.uploading-progress 0%
|
||||
= loading_icon(css_class: 'align-text-bottom gl-mr-2')
|
||||
|
||||
%button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
|
||||
= sprite_icon('media')
|
||||
%span.gl-button-text
|
||||
= _("Attach a file")
|
||||
%span.uploading-error-container.hide
|
||||
%span.uploading-error-icon
|
||||
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
|
||||
%span.uploading-error-message
|
||||
-# Populated by app/assets/javascripts/dropzone_input.js
|
||||
%button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
|
||||
%span.gl-button-text
|
||||
= _("Try again")
|
||||
= _("or")
|
||||
%button.btn.gl-button.btn-link.attach-new-file.markdown-selector.gl-vertical-align-baseline
|
||||
%span.gl-button-text
|
||||
= _("attach a new file")
|
||||
= _(".")
|
||||
|
||||
%button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
|
||||
%span.gl-button-text
|
||||
= _("Cancel")
|
||||
%button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
|
||||
= sprite_icon('media')
|
||||
%span.gl-button-text
|
||||
= _("Attach a file")
|
||||
|
||||
%button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
|
||||
%span.gl-button-text
|
||||
= _("Cancel")
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
= form_tag page_filter_path, method: :get, class: "topic-filter-form js-topic-filter-form", id: 'topic-filter-form' do |f|
|
||||
= search_field_tag :search, params[:search],
|
||||
placeholder: s_('Filter by name'),
|
||||
class: 'topic-filter-form-field form-control input-short',
|
||||
spellcheck: false,
|
||||
id: 'topic-filter-form-field',
|
||||
autofocus: local_assigns[:autofocus]
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_runner_limits
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60157
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329438
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::runner
|
||||
default_enabled: false
|
|
@ -7,7 +7,8 @@ product_stage: package
|
|||
product_group: group::package
|
||||
product_category: container registry
|
||||
value_type: number
|
||||
status: deprecated
|
||||
status: removed
|
||||
milestone_removed: "14.4"
|
||||
time_frame: all
|
||||
data_source: redis
|
||||
distribution:
|
||||
|
|
|
@ -61,6 +61,13 @@ namespace :admin do
|
|||
end
|
||||
end
|
||||
|
||||
resources :topics, only: [:index, :new, :create, :edit, :update] do
|
||||
resource :avatar, controller: 'topics/avatars', only: [:destroy]
|
||||
collection do
|
||||
post :preview_markdown
|
||||
end
|
||||
end
|
||||
|
||||
resources :deploy_keys, only: [:index, :new, :create, :edit, :update, :destroy]
|
||||
|
||||
resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
scope path: :uploads do
|
||||
# Note attachments and User/Group/Project avatars
|
||||
# Note attachments and User/Group/Project/Topic avatars
|
||||
get "-/system/:model/:mounted_as/:id/:filename",
|
||||
to: "uploads#show",
|
||||
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
|
||||
constraints: { model: %r{note|user|group|project|projects\/topic}, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
|
||||
|
||||
# show uploads for models, snippets (notes) available for now
|
||||
get '-/system/:model/:id/:secret/:filename',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTopicsNameGinIndex < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_topics_on_name_trigram'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :topics, :name, name: INDEX_NAME, using: :gin, opclass: { name: :gin_trgm_ops }
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :topics, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTopicsTotalProjectsCountCache < Gitlab::Database::Migration[1.0]
|
||||
def up
|
||||
add_column :topics, :total_projects_count, :bigint, null: false, default: 0
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :topics, :total_projects_count
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTopicsTotalProjectsCountIndex < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_topics_total_projects_count'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :topics, [:total_projects_count, :id], order: { total_projects_count: :desc }, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :topics, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RecreateIndexSecurityCiBuildsOnNameAndIdParserFeatures < Gitlab::Database::Migration[1.0]
|
||||
TABLE = "ci_builds"
|
||||
OLD_INDEX_NAME = "index_security_ci_builds_on_name_and_id_parser_features"
|
||||
NEW_INDEX_NAME = "index_security_ci_builds_on_name_and_id_parser_features_broken"
|
||||
COLUMNS = %i[name id]
|
||||
CONSTRAINTS = "(name::text = ANY (ARRAY['container_scanning'::character varying::text,
|
||||
'dast'::character varying::text,
|
||||
'dependency_scanning'::character varying::text,
|
||||
'license_management'::character varying::text,
|
||||
'sast'::character varying::text,
|
||||
'secret_detection'::character varying::text,
|
||||
'coverage_fuzzing'::character varying::text,
|
||||
'license_scanning'::character varying::text])
|
||||
) AND type::text = 'Ci::Build'::text"
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
rename_index(TABLE, OLD_INDEX_NAME, NEW_INDEX_NAME)
|
||||
prepare_async_index TABLE, COLUMNS, name: OLD_INDEX_NAME, where: CONSTRAINTS
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index TABLE, COLUMNS, name: OLD_INDEX_NAME
|
||||
rename_index(TABLE, NEW_INDEX_NAME, OLD_INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SchedulePopulateTopicsTotalProjectsCountCache < Gitlab::Database::Migration[1.0]
|
||||
MIGRATION = 'PopulateTopicsTotalProjectsCountCache'
|
||||
BATCH_SIZE = 10_000
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
queue_background_migration_jobs_by_range_at_intervals(
|
||||
define_batchable_model('topics'),
|
||||
MIGRATION,
|
||||
DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
track_jobs: true
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
e035616201329b7610e8c3a647bc01c52ce722790ea7bb88d4a38bc0feb4737e
|
|
@ -0,0 +1 @@
|
|||
37016ec5e5ab1bd8d2bd8020f98277b3ad9f450b833ce3ebde70aebce5130a26
|
|
@ -0,0 +1 @@
|
|||
0d6ec7c1d96f32c645ddc051d8e3b3bd0ad759c52c8938888287b1c6b57d27a3
|
|
@ -0,0 +1 @@
|
|||
918852db691546e4e93a933789968115ac98b5757d480ed1e09118508e6024d5
|
|
@ -0,0 +1 @@
|
|||
19efbbf7aab5837e33ff72d87e101a76da7eeb1d60c05ffc0ceddad1d0cbc69c
|
|
@ -19667,6 +19667,7 @@ CREATE TABLE topics (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
avatar text,
|
||||
description text,
|
||||
total_projects_count bigint DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_26753fb43a CHECK ((char_length(avatar) <= 255)),
|
||||
CONSTRAINT check_5d1a07c8c8 CHECK ((char_length(description) <= 1024)),
|
||||
CONSTRAINT check_7a90d4c757 CHECK ((char_length(name) <= 255))
|
||||
|
@ -26518,7 +26519,7 @@ CREATE UNIQUE INDEX index_scim_oauth_access_tokens_on_group_id_and_token_encrypt
|
|||
|
||||
CREATE INDEX index_secure_ci_builds_on_user_id_name_created_at ON ci_builds USING btree (user_id, name, created_at) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('apifuzzer_fuzz'::character varying)::text, ('apifuzzer_fuzz_dnd'::character varying)::text, ('secret_detection'::character varying)::text])));
|
||||
|
||||
CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features ON ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
|
||||
CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features_broken ON ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
|
||||
|
||||
CREATE INDEX index_security_findings_on_confidence ON security_findings USING btree (confidence);
|
||||
|
||||
|
@ -26742,6 +26743,10 @@ CREATE UNIQUE INDEX index_token_with_ivs_on_hashed_token ON token_with_ivs USING
|
|||
|
||||
CREATE UNIQUE INDEX index_topics_on_name ON topics USING btree (name);
|
||||
|
||||
CREATE INDEX index_topics_on_name_trigram ON topics USING gin (name gin_trgm_ops);
|
||||
|
||||
CREATE INDEX index_topics_total_projects_count ON topics USING btree (total_projects_count DESC, id);
|
||||
|
||||
CREATE UNIQUE INDEX index_trending_projects_on_project_id ON trending_projects USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING btree (key_handle);
|
||||
|
|
|
@ -534,7 +534,11 @@ Plan.default.actual_limits.update!(pages_file_entries: 100)
|
|||
|
||||
### Number of registered runners per scope
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12. Disabled by default.
|
||||
> - Enabled on GitLab.com in GitLab 14.3.
|
||||
> - Enabled on self-managed in GitLab 14.4.
|
||||
> - Feature flag `ci_runner_limits` removed in GitLab 14.4. You can still use `ci_runner_limits_override`
|
||||
to remove limits for a given scope.
|
||||
|
||||
The total number of registered runners is limited at the group and project levels. Each time a new runner is registered,
|
||||
GitLab checks these limits against runners that have been active in the last 3 months.
|
||||
|
|
|
@ -230,7 +230,7 @@ reports.
|
|||
## Monthly release process (versions)
|
||||
|
||||
The docs website supports versions and each month we add the latest one to the list.
|
||||
For more information, read about the [monthly release process](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#monthly-documentation-releases).
|
||||
For more information, read about the [monthly release process](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/releases.md).
|
||||
|
||||
## Review Apps for documentation merge requests
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ There are many places where file uploading is used, according to contexts:
|
|||
- LFS Objects
|
||||
- Merge request diffs
|
||||
- Design Management design thumbnails
|
||||
- Topic
|
||||
- Topic avatars
|
||||
|
||||
## Disk storage
|
||||
|
||||
|
@ -42,6 +44,7 @@ they are still not 100% standardized. You can see them below:
|
|||
| User avatars | yes | `uploads/-/system/user/avatar/:id/:filename` | `AvatarUploader` | User |
|
||||
| User snippet attachments | yes | `uploads/-/system/personal_snippet/:id/:random_hex/:filename` | `PersonalFileUploader` | Snippet |
|
||||
| Project avatars | yes | `uploads/-/system/project/avatar/:id/:filename` | `AvatarUploader` | Project |
|
||||
| Topic avatars | yes | `uploads/-/system/projects/topic/avatar/:id/:filename` | `AvatarUploader` | Topic |
|
||||
| Issues/MR/Notes Markdown attachments | yes | `uploads/:project_path_with_namespace/:random_hex/:filename` | `FileUploader` | Project |
|
||||
| Issues/MR/Notes Legacy Markdown attachments | no | `uploads/-/system/note/attachment/:id/:filename` | `AttachmentUploader` | Note |
|
||||
| Design Management design thumbnails | yes | `uploads/-/system/design_management/action/image_v432x230/:id/:filename` | `DesignManagement::DesignV432x230Uploader` | DesignManagement::Action |
|
||||
|
|
|
@ -231,7 +231,6 @@ graph RL;
|
|||
1-16["brakeman-sast"];
|
||||
1-17["eslint-sast"];
|
||||
1-18["kubesec-sast"];
|
||||
1-19["nodejs-scan-sast"];
|
||||
1-20["secrets-sast"];
|
||||
1-21["static-analysis (14 minutes)"];
|
||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||
|
@ -333,7 +332,6 @@ graph RL;
|
|||
1-16["brakeman-sast"];
|
||||
1-17["eslint-sast"];
|
||||
1-18["kubesec-sast"];
|
||||
1-19["nodejs-scan-sast"];
|
||||
1-20["secrets-sast"];
|
||||
1-21["static-analysis (14 minutes)"];
|
||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||
|
@ -459,7 +457,6 @@ graph RL;
|
|||
1-16["brakeman-sast"];
|
||||
1-17["eslint-sast"];
|
||||
1-18["kubesec-sast"];
|
||||
1-19["nodejs-scan-sast"];
|
||||
1-20["secrets-sast"];
|
||||
1-21["static-analysis (14 minutes)"];
|
||||
click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
|
||||
|
|
|
@ -338,6 +338,7 @@ Below is a list of Mattermost versions for GitLab 11.10 and later:
|
|||
| 14.1 | 5.36 |
|
||||
| 14.2 | 5.37 |
|
||||
| 14.3 | 5.38 |
|
||||
| 14.4 | 5.39 |
|
||||
|
||||
NOTE:
|
||||
When upgrading the Mattermost version, it is essential to check the
|
||||
|
|
|
@ -18,21 +18,23 @@ To add a deprecation, use the example.yml file in `/data/deprecations/templates`
|
|||
then run `bin/rake gitlab:docs:compile_deprecations`.
|
||||
-->
|
||||
|
||||
## 15.2
|
||||
## 14.4
|
||||
|
||||
### NFS for Git repository storage deprecated
|
||||
### Rename Task Runner pod to Toolbox
|
||||
|
||||
With the general availability of Gitaly Cluster ([introduced in GitLab 13.0](https://about.gitlab.com/releases/2020/05/22/gitlab-13-0-released/)), we have deprecated development (bugfixes, performance improvements, etc) for NFS for Git repository storage in GitLab 14.0. We will continue to provide technical support for NFS for Git repositories throughout 14.x, but we will remove all support for NFS in GitLab 15.0. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs) for further information.
|
||||
The Task Runner pod is used to execute periodic housekeeping tasks within the GitLab application and is often confused with the GitLab Runner. Thus, [Task Runner will be renamed to Toolbox](https://gitlab.com/groups/gitlab-org/charts/-/epics/25).
|
||||
|
||||
Gitaly Cluster offers tremendous benefits for our customers such as:
|
||||
This will result in the rename of the sub-chart: `gitlab/task-runner` to `gitlab/toolbox`. Resulting pods will be named along the lines of `{{ .Release.Name }}-toolbox`, which will often be `gitlab-toolbox`. They will be locatable with the label `app=toolbox`.
|
||||
|
||||
- [Variable replication factors](https://docs.gitlab.com/ee/administration/gitaly/index.html#replication-factor).
|
||||
- [Strong consistency](https://docs.gitlab.com/ee/administration/gitaly/index.html#strong-consistency).
|
||||
- [Distributed read capabilities](https://docs.gitlab.com/ee/administration/gitaly/index.html#distributed-reads).
|
||||
Announced: 2021-08-22
|
||||
|
||||
We encourage customers currently using NFS for Git repositories to plan their migration by reviewing our documentation on [migrating to Gitaly Cluster](https://docs.gitlab.com/ee/administration/gitaly/index.html#migrate-to-gitaly-cluster).
|
||||
## 14.6
|
||||
|
||||
Announced: 2021-06-22
|
||||
### Release CLI be distributed as a generic package
|
||||
|
||||
The [release-cli](https://gitlab.com/gitlab-org/release-cli) will be released as a [generic package](https://gitlab.com/gitlab-org/release-cli/-/packages) starting in GitLab 14.2. We will continue to deploy it as a binary to S3 until GitLab 14.5 and stop distributing it in S3 in GitLab 14.6.
|
||||
|
||||
Announced: 2021-08-22
|
||||
|
||||
## 15.0
|
||||
|
||||
|
@ -74,20 +76,18 @@ We decided to remove the GitLab Serverless features as they never really resonat
|
|||
|
||||
Announced: 2021-09-22
|
||||
|
||||
## 14.6
|
||||
## 15.2
|
||||
|
||||
### Release CLI be distributed as a generic package
|
||||
### NFS for Git repository storage deprecated
|
||||
|
||||
The [release-cli](https://gitlab.com/gitlab-org/release-cli) will be released as a [generic package](https://gitlab.com/gitlab-org/release-cli/-/packages) starting in GitLab 14.2. We will continue to deploy it as a binary to S3 until GitLab 14.5 and stop distributing it in S3 in GitLab 14.6.
|
||||
With the general availability of Gitaly Cluster ([introduced in GitLab 13.0](https://about.gitlab.com/releases/2020/05/22/gitlab-13-0-released/)), we have deprecated development (bugfixes, performance improvements, etc) for NFS for Git repository storage in GitLab 14.0. We will continue to provide technical support for NFS for Git repositories throughout 14.x, but we will remove all support for NFS in GitLab 15.0. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs) for further information.
|
||||
|
||||
Announced: 2021-08-22
|
||||
Gitaly Cluster offers tremendous benefits for our customers such as:
|
||||
|
||||
## 14.4
|
||||
- [Variable replication factors](https://docs.gitlab.com/ee/administration/gitaly/index.html#replication-factor).
|
||||
- [Strong consistency](https://docs.gitlab.com/ee/administration/gitaly/index.html#strong-consistency).
|
||||
- [Distributed read capabilities](https://docs.gitlab.com/ee/administration/gitaly/index.html#distributed-reads).
|
||||
|
||||
### Rename Task Runner pod to Toolbox
|
||||
We encourage customers currently using NFS for Git repositories to plan their migration by reviewing our documentation on [migrating to Gitaly Cluster](https://docs.gitlab.com/ee/administration/gitaly/index.html#migrate-to-gitaly-cluster).
|
||||
|
||||
The Task Runner pod is used to execute periodic housekeeping tasks within the GitLab application and is often confused with the GitLab Runner. Thus, [Task Runner will be renamed to Toolbox](https://gitlab.com/groups/gitlab-org/charts/-/epics/25).
|
||||
|
||||
This will result in the rename of the sub-chart: `gitlab/task-runner` to `gitlab/toolbox`. Resulting pods will be named along the lines of `{{ .Release.Name }}-toolbox`, which will often be `gitlab-toolbox`. They will be locatable with the label `app=toolbox`.
|
||||
|
||||
Announced: 2021-08-22
|
||||
Announced: 2021-06-22
|
||||
|
|
|
@ -24,7 +24,7 @@ The Admin Area is made up of the following sections:
|
|||
|
||||
| Section | Description |
|
||||
|:-----------------------------------------------|:------------|
|
||||
| **{overview}** [Overview](#overview-section) | View your GitLab [Dashboard](#admin-area-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [jobs](#administering-jobs), [runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). |
|
||||
| **{overview}** [Overview](#overview-section) | View your GitLab [Dashboard](#admin-area-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [topics](#administering-topics), [jobs](#administering-jobs), [runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). |
|
||||
| **{monitor}** Monitoring | View GitLab [system information](#system-information), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), [requests profiles](#requests-profiles), and [audit events](#audit-events). |
|
||||
| **{messages}** Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
|
||||
| **{hook}** System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. |
|
||||
|
@ -237,6 +237,26 @@ insensitive, and applies partial matching.
|
|||
|
||||
To [Create a new group](../group/index.md#create-a-group) click **New group**.
|
||||
|
||||
### Administering Topics
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340920) in GitLab 14.4.
|
||||
|
||||
You can administer all topics in the GitLab instance from the Admin Area's Topics page.
|
||||
|
||||
To access the Topics page:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Overview > Topics**.
|
||||
|
||||
For each topic, the page displays their name and number of projects labeled with the topic.
|
||||
|
||||
To create a new topic, select **New topic**.
|
||||
|
||||
To edit a topic, select **Edit** in that topic's row.
|
||||
|
||||
To search for topics by name, enter your criteria in the search box. The topic search is case
|
||||
insensitive, and applies partial matching.
|
||||
|
||||
### Administering Jobs
|
||||
|
||||
You can administer all jobs in the GitLab instance from the Admin Area's Jobs page.
|
||||
|
|
|
@ -302,11 +302,12 @@ best practices. You can customize this flow by adding, hiding or re-ordering sta
|
|||
|
||||
To create a value stream:
|
||||
|
||||
1. Navigate to your group's **Analytics > Value Stream**.
|
||||
1. Click the Value stream dropdown and select **Create new Value Stream**
|
||||
1. Fill in a name for the new Value Stream
|
||||
- You can [customize the stages](#creating-a-value-stream-with-stages)
|
||||
1. Click the **Create Value Stream** button.
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. In the top right, select the dropdown list and then **Create new Value Stream**.
|
||||
1. Enter a name for the new Value Stream.
|
||||
- You can [customize the stages](#creating-a-value-stream-with-stages).
|
||||
1. Select **Create Value Stream**.
|
||||
|
||||
![New value stream](img/new_value_stream_v13_12.png "Creating a new value stream")
|
||||
|
||||
|
@ -324,18 +325,19 @@ add stages as desired.
|
|||
|
||||
To create a value stream with stages:
|
||||
|
||||
1. Go to your group and select **Analytics > Value Stream**.
|
||||
1. Select the Value Stream dropdown and select **Create new Value Stream**.
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. In the top right, select the dropdown list and then **Create new Value Stream**.
|
||||
1. Select either **Create from default template** or **Create from no template**.
|
||||
- Default stages in the value stream can be hidden or re-ordered.
|
||||
- You can hide or re-order default stages in the value stream.
|
||||
|
||||
![Default stage actions](img/vsa_default_stage_v13_10.png "Default stage actions")
|
||||
|
||||
- New stages can be added by clicking the 'Add another stage' button.
|
||||
- The name, start and end events for the stage can be selected
|
||||
- You can add new stages by selecting **Add another stage**.
|
||||
- You can select the name and start and end events for the stage.
|
||||
|
||||
![Custom stage actions](img/vsa_custom_stage_v13_10.png "Custom stage actions")
|
||||
1. Select the **Create Value Stream** button to save the value stream.
|
||||
1. Select **Create Value Stream**.
|
||||
|
||||
#### Label-based stages
|
||||
|
||||
|
@ -358,8 +360,9 @@ In this example, we'd like to measure times for deployment from a staging enviro
|
|||
|
||||
After you create a value stream, you can customize it to suit your purposes. To edit a value stream:
|
||||
|
||||
1. Go to your group and select **Analytics > Value Stream**.
|
||||
1. Find and select the relevant value stream from the value stream dropdown.
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. In the top right, select the dropdown list and then select the relevant value stream.
|
||||
1. Next to the value stream dropdown, select **Edit**.
|
||||
The edit form is populated with the value stream details.
|
||||
1. Optional:
|
||||
|
@ -377,10 +380,11 @@ After you create a value stream, you can customize it to suit your purposes. To
|
|||
|
||||
To delete a custom value stream:
|
||||
|
||||
1. Navigate to your group's **Analytics > Value Stream**.
|
||||
1. Click the Value stream dropdown and select the value stream you would like to delete.
|
||||
1. Click the **Delete (name of value stream)**.
|
||||
1. Click the **Delete** button to confirm.
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. In the top right, select the dropdown list and then select the value stream you would like to delete.
|
||||
1. Select **Delete (name of value stream)**.
|
||||
1. To confirm, select **Delete**.
|
||||
|
||||
![Delete value stream](img/delete_value_stream_v13_12.png "Deleting a custom value stream")
|
||||
|
||||
|
@ -413,7 +417,7 @@ select up to a total of 15 labels.
|
|||
|
||||
## Permissions
|
||||
|
||||
To access Group-level Value Stream Analytics, users must have Reporter access or above.
|
||||
To access Group-level Value Stream Analytics, users must have at least the Reporter role.
|
||||
|
||||
You can [read more about permissions](../../permissions.md) in general.
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
SUB_BATCH_SIZE = 1_000
|
||||
|
||||
# The class to populates the total projects counter cache of topics
|
||||
class PopulateTopicsTotalProjectsCountCache
|
||||
# Temporary AR model for topics
|
||||
class Topic < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'topics'
|
||||
end
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch|
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql})
|
||||
UPDATE topics
|
||||
SET total_projects_count = (SELECT COUNT(*) FROM project_topics WHERE topic_id = batched_relation.id)
|
||||
FROM batched_relation
|
||||
WHERE topics.id = batched_relation.id
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -73,7 +73,7 @@ module Gitlab
|
|||
# To prevent this from happening, we scope sticking to all the
|
||||
# models that support load balancing. In the future (if we
|
||||
# determined this to be OK) we may be able to relax this.
|
||||
LoadBalancing.base_models.map do |model|
|
||||
::Gitlab::Database::LoadBalancing.base_models.map do |model|
|
||||
[model, :user, warden.user.id]
|
||||
end
|
||||
elsif env[STICK_OBJECT].present?
|
||||
|
|
|
@ -74,7 +74,7 @@ module Gitlab
|
|||
def unstick_or_continue_sticking(namespace, id)
|
||||
return if all_caught_up?(namespace, id)
|
||||
|
||||
Session.current.use_primary!
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_primary!
|
||||
end
|
||||
|
||||
# Select a replica that has caught up with the primary. If one has not been
|
||||
|
@ -83,14 +83,14 @@ module Gitlab
|
|||
replica_selected =
|
||||
select_caught_up_replicas(namespace, id)
|
||||
|
||||
Session.current.use_primary! unless replica_selected
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_primary! unless replica_selected
|
||||
end
|
||||
|
||||
# Starts sticking to the primary for the given namespace and id, using
|
||||
# the latest WAL pointer from the primary.
|
||||
def stick(namespace, id)
|
||||
mark_primary_write_location(namespace, id)
|
||||
Session.current.use_primary!
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_primary!
|
||||
end
|
||||
|
||||
def bulk_stick(namespace, ids)
|
||||
|
@ -100,7 +100,7 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
Session.current.use_primary!
|
||||
::Gitlab::Database::LoadBalancing::Session.current.use_primary!
|
||||
end
|
||||
|
||||
def with_primary_write_location
|
||||
|
|
|
@ -9,6 +9,7 @@ module Gitlab
|
|||
ee/lib/ee/peek
|
||||
lib/peek
|
||||
lib/gitlab/database
|
||||
lib/gitlab/gitaly_client.rb
|
||||
lib/gitlab/gitaly_client/call.rb
|
||||
lib/gitlab/instrumentation/redis_interceptor.rb
|
||||
].freeze
|
||||
|
|
|
@ -2108,6 +2108,9 @@ msgstr ""
|
|||
msgid "Add to tree"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add topics to projects to help users find them."
|
||||
msgstr ""
|
||||
|
||||
msgid "Add trigger"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9708,6 +9711,9 @@ msgstr ""
|
|||
msgid "Create tag %{tagName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create user"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12369,6 +12375,9 @@ msgstr ""
|
|||
msgid "Edit title and description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit topic: %{topic_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit user: %{user_name}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22390,6 +22399,9 @@ msgstr ""
|
|||
msgid "My company or team"
|
||||
msgstr ""
|
||||
|
||||
msgid "My topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "My-Reaction"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22883,6 +22895,9 @@ msgstr ""
|
|||
msgid "New test case"
|
||||
msgstr ""
|
||||
|
||||
msgid "New topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "New users set to external"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25571,6 +25586,9 @@ msgstr ""
|
|||
msgid "Please fill in a descriptive name for your group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please fill in a name for your topic."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please fill out this field."
|
||||
msgstr ""
|
||||
|
||||
|
@ -34347,6 +34365,9 @@ msgstr ""
|
|||
msgid "There are no projects shared with this group yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no topics to show."
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no variables yet."
|
||||
msgstr ""
|
||||
|
||||
|
@ -35839,6 +35860,21 @@ msgstr ""
|
|||
msgid "TopNav|Go back"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic %{topic_name} was successfully created."
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic was successfully updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Topics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topics (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -38705,6 +38741,9 @@ msgstr ""
|
|||
msgid "Write a description or drag your files here…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Write a description…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Write milestone description..."
|
||||
msgstr ""
|
||||
|
||||
|
@ -39619,6 +39658,21 @@ msgstr[1] ""
|
|||
msgid "Your username is %{username}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|Failed to load ZenTao issue. View the issue in ZenTao, or reload the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}ZenTao%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|This is a ZenTao user."
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|This issue is synchronized with ZenTao"
|
||||
msgstr ""
|
||||
|
||||
msgid "ZenTaoIntegration|ZenTao user"
|
||||
msgstr ""
|
||||
|
||||
msgid "ZentaoIntegration|Base URL of the Zentao instance."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::Topics::AvatarsController do
|
||||
let(:user) { create(:admin) }
|
||||
let(:topic) { create(:topic, avatar: fixture_file_upload("spec/fixtures/dk.png")) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
controller.instance_variable_set(:@topic, topic)
|
||||
end
|
||||
|
||||
it 'removes avatar from DB by calling destroy' do
|
||||
delete :destroy, params: { topic_id: topic.id }
|
||||
@topic = assigns(:topic)
|
||||
expect(@topic.avatar.present?).to be_falsey
|
||||
expect(@topic).to be_valid
|
||||
end
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::TopicsController do
|
||||
let_it_be(:topic) { create(:topic, name: 'topic') }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'renders the template' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('index')
|
||||
end
|
||||
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404 error' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
it 'renders the template' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('new')
|
||||
end
|
||||
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404 error' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
it 'renders the template' do
|
||||
get :edit, params: { id: topic.id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('edit')
|
||||
end
|
||||
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404 error' do
|
||||
get :edit, params: { id: topic.id }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
it 'creates topic' do
|
||||
expect do
|
||||
post :create, params: { projects_topic: { name: 'test' } }
|
||||
end.to change { Projects::Topic.count }.by(1)
|
||||
end
|
||||
|
||||
it 'shows error message for invalid topic' do
|
||||
post :create, params: { projects_topic: { name: nil } }
|
||||
|
||||
errors = assigns[:topic].errors
|
||||
expect(errors).to contain_exactly(errors.full_message(:name, I18n.t('errors.messages.blank')))
|
||||
end
|
||||
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404 error' do
|
||||
post :create, params: { projects_topic: { name: 'test' } }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
it 'updates topic' do
|
||||
put :update, params: { id: topic.id, projects_topic: { name: 'test' } }
|
||||
|
||||
expect(response).to redirect_to(edit_admin_topic_path(topic))
|
||||
expect(topic.reload.name).to eq('test')
|
||||
end
|
||||
|
||||
it 'shows error message for invalid topic' do
|
||||
put :update, params: { id: topic.id, projects_topic: { name: nil } }
|
||||
|
||||
errors = assigns[:topic].errors
|
||||
expect(errors).to contain_exactly(errors.full_message(:name, I18n.t('errors.messages.blank')))
|
||||
end
|
||||
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'renders a 404 error' do
|
||||
put :update, params: { id: topic.id, projects_topic: { name: 'test' } }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -599,6 +599,46 @@ RSpec.describe UploadsController do
|
|||
end
|
||||
end
|
||||
|
||||
context "when viewing a topic avatar" do
|
||||
let!(:topic) { create(:topic, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
|
||||
|
||||
context "when signed in" do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "responds with status 200" do
|
||||
get :show, params: { model: "projects/topic", mounted_as: "avatar", id: topic.id, filename: "dk.png" }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it_behaves_like 'content publicly cached' do
|
||||
subject do
|
||||
get :show, params: { model: "projects/topic", mounted_as: "avatar", id: topic.id, filename: "dk.png" }
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when not signed in" do
|
||||
it "responds with status 200" do
|
||||
get :show, params: { model: "projects/topic", mounted_as: "avatar", id: topic.id, filename: "dk.png" }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it_behaves_like 'content publicly cached' do
|
||||
subject do
|
||||
get :show, params: { model: "projects/topic", mounted_as: "avatar", id: topic.id, filename: "dk.png" }
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Appearance' do
|
||||
context 'when viewing a custom header logo' do
|
||||
let!(:appearance) { create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::TopicsFinder do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let!(:topic1) { create(:topic, name: 'topicB') }
|
||||
let!(:topic2) { create(:topic, name: 'topicC') }
|
||||
let!(:topic3) { create(:topic, name: 'topicA') }
|
||||
|
||||
let!(:project1) { create(:project, namespace: user.namespace, topic_list: 'topicC, topicA, topicB') }
|
||||
let!(:project2) { create(:project, namespace: user.namespace, topic_list: 'topicC, topicA') }
|
||||
let!(:project3) { create(:project, namespace: user.namespace, topic_list: 'topicC') }
|
||||
|
||||
describe '#execute' do
|
||||
it 'returns topics' do
|
||||
topics = described_class.new.execute
|
||||
|
||||
expect(topics).to eq([topic2, topic3, topic1])
|
||||
end
|
||||
|
||||
context 'filter by name' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:search, :result) do
|
||||
'topic' | %w[topicC topicA topicB]
|
||||
'pic' | %w[topicC topicA topicB]
|
||||
'B' | %w[]
|
||||
'cB' | %w[]
|
||||
'icB' | %w[topicB]
|
||||
'topicA' | %w[topicA]
|
||||
'topica' | %w[topicA]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns filtered topics' do
|
||||
topics = described_class.new(params: { search: search }).execute
|
||||
|
||||
expect(topics.map(&:name)).to eq(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -110,6 +110,42 @@
|
|||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"warnings": []
|
||||
}, {
|
||||
"start": 9081.502219885,
|
||||
"feature": "commit_service#find_commit",
|
||||
"duration": 6.678,
|
||||
"request": "{:repository=>\n {:storage_name=>\"nfs-file-cny01\",\n :relative_path=>\n \"@hashed/a6/80/a68072e80f075e89bc74a300101a9e71e8363bdb542182580162553462480a52.git\",\n :git_object_directory=>\"\",\n :git_alternate_object_directories=>[],\n :gl_repository=>\"project-278964\",\n :gl_project_path=>\"gitlab-org/gitlab\"},\n :revision=>\"master\"}\n",
|
||||
"rpc": "find_commit",
|
||||
"backtrace": [
|
||||
"lib/gitlab/gitaly_client/call.rb:30:in `call'",
|
||||
"lib/gitlab/gitaly_client.rb:167:in `call'",
|
||||
"lib/gitlab/gitaly_client/commit_service.rb:520:in `call_find_commit'",
|
||||
"lib/gitlab/gitaly_client/commit_service.rb:354:in `find_commit'",
|
||||
"lib/gitlab/git/commit.rb:74:in `block in find_commit'",
|
||||
"lib/gitlab/git/wraps_gitaly_errors.rb:7:in `wrapped_gitaly_errors'",
|
||||
"lib/gitlab/git/commit.rb:73:in `find_commit'",
|
||||
"lib/gitlab/git/rugged_impl/commit.rb:41:in `find_commit'",
|
||||
"lib/gitlab/git/commit.rb:65:in `find'",
|
||||
"lib/gitlab/git/repository.rb:789:in `commit'",
|
||||
"app/services/branches/diverging_commit_counts_service.rb:21:in `diverging_commit_counts'",
|
||||
"app/services/branches/diverging_commit_counts_service.rb:11:in `call'",
|
||||
"app/controllers/projects/branches_controller.rb:57:in `block (4 levels) in diverging_commit_counts'",
|
||||
"app/controllers/projects/branches_controller.rb:57:in `to_h'",
|
||||
"app/controllers/projects/branches_controller.rb:57:in `block (3 levels) in diverging_commit_counts'",
|
||||
"lib/gitlab/gitaly_client.rb:325:in `allow_n_plus_1_calls'",
|
||||
"app/controllers/projects/branches_controller.rb:56:in `block (2 levels) in diverging_commit_counts'",
|
||||
"app/controllers/projects/branches_controller.rb:51:in `diverging_commit_counts'",
|
||||
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
|
||||
"ee/app/controllers/ee/application_controller.rb:44:in `set_current_ip_address'",
|
||||
"app/controllers/application_controller.rb:497:in `set_current_admin'",
|
||||
"lib/gitlab/session.rb:11:in `with_session'",
|
||||
"app/controllers/application_controller.rb:488:in `set_session_storage'",
|
||||
"app/controllers/application_controller.rb:482:in `set_locale'",
|
||||
"app/controllers/application_controller.rb:476:in `set_current_context'",
|
||||
"ee/lib/omni_auth/strategies/group_saml.rb:41:in `other_phase'",
|
||||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"warnings": []
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
|
||||
|
||||
import * as actions from '~/add_context_commits_modal/store/actions';
|
||||
import mutations from '~/add_context_commits_modal/store/mutations';
|
||||
import defaultState from '~/add_context_commits_modal/store/state';
|
||||
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -18,7 +18,7 @@ describe('AddContextCommitsModal', () => {
|
|||
const removeContextCommits = jest.fn();
|
||||
const resetModalState = jest.fn();
|
||||
const searchCommits = jest.fn();
|
||||
const { commit } = getDiffWithCommit();
|
||||
const { commit } = getDiffWithCommit;
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
store = new Vuex.Store({
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
|
||||
import CommitItem from '~/diffs/components/commit_item.vue';
|
||||
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
|
||||
|
||||
describe('ReviewTabContainer', () => {
|
||||
let wrapper;
|
||||
const { commit } = getDiffWithCommit();
|
||||
const { commit } = getDiffWithCommit;
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
wrapper = shallowMount(ReviewTabContainer, {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import * as types from '~/add_context_commits_modal/store/mutation_types';
|
||||
import mutations from '~/add_context_commits_modal/store/mutations';
|
||||
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
|
||||
|
||||
describe('AddContextCommitsModalStoreMutations', () => {
|
||||
const { commit } = getDiffWithCommit();
|
||||
const { commit } = getDiffWithCommit;
|
||||
describe('SET_BASE_CONFIG', () => {
|
||||
it('should set contextCommitsPath, mergeRequestIid and projectId', () => {
|
||||
const state = {};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import Component from '~/diffs/components/commit_item.vue';
|
||||
import { getTimeago } from '~/lib/utils/datetime_utility';
|
||||
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
|
||||
import getDiffWithCommit from '../mock_data/diff_with_commit';
|
||||
|
||||
jest.mock('~/user_popovers');
|
||||
|
||||
|
@ -18,7 +18,7 @@ describe('diffs/components/commit_item', () => {
|
|||
let wrapper;
|
||||
|
||||
const timeago = getTimeago();
|
||||
const { commit } = getDiffWithCommit();
|
||||
const { commit } = getDiffWithCommit;
|
||||
|
||||
const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
|
||||
const getDescElement = () => wrapper.find('pre.commit-row-description');
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import getDiffWithCommit from '../mock_data/diff_with_commit';
|
||||
import diffsMockData from '../mock_data/merge_request_diffs';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -22,7 +22,7 @@ describe('CompareVersions', () => {
|
|||
let wrapper;
|
||||
let store;
|
||||
const targetBranchName = 'tmp-wine-dev';
|
||||
const { commit } = getDiffWithCommit();
|
||||
const { commit } = getDiffWithCommit;
|
||||
|
||||
const createWrapper = (props = {}, commitArgs = {}, createCommit = true) => {
|
||||
if (createCommit) {
|
||||
|
@ -150,7 +150,7 @@ describe('CompareVersions', () => {
|
|||
|
||||
describe('commit', () => {
|
||||
beforeEach(() => {
|
||||
store.state.diffs.commit = getDiffWithCommit().commit;
|
||||
store.state.diffs.commit = getDiffWithCommit.commit;
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import fixture from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
|
||||
export default function getDiffWithCommit() {
|
||||
return fixture;
|
||||
}
|
|
@ -7,7 +7,7 @@ RSpec.describe AvatarsHelper do
|
|||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe '#project_icon & #group_icon' do
|
||||
describe '#project_icon, #group_icon, #topic_icon' do
|
||||
shared_examples 'resource with a default avatar' do |source_type|
|
||||
it 'returns a default avatar div' do
|
||||
expect(public_send("#{source_type}_icon", *helper_args))
|
||||
|
@ -71,6 +71,18 @@ RSpec.describe AvatarsHelper do
|
|||
let(:helper_args) { [resource] }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when providing a topic' do
|
||||
it_behaves_like 'resource with a default avatar', 'topic' do
|
||||
let(:resource) { create(:topic, name: 'foo') }
|
||||
let(:helper_args) { [resource] }
|
||||
end
|
||||
|
||||
it_behaves_like 'resource with a custom avatar', 'topic' do
|
||||
let(:resource) { create(:topic, avatar: File.open(uploaded_image_temp_path)) }
|
||||
let(:helper_args) { [resource] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#avatar_icon_for' do
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsTotalProjectsCountCache, schema: 20211006060436 do
|
||||
it 'correctly populates total projects count cache' do
|
||||
namespaces = table(:namespaces)
|
||||
projects = table(:projects)
|
||||
topics = table(:topics)
|
||||
project_topics = table(:project_topics)
|
||||
|
||||
group = namespaces.create!(name: 'group', path: 'group')
|
||||
project_1 = projects.create!(namespace_id: group.id)
|
||||
project_2 = projects.create!(namespace_id: group.id)
|
||||
project_3 = projects.create!(namespace_id: group.id)
|
||||
topic_1 = topics.create!(name: 'Topic1')
|
||||
topic_2 = topics.create!(name: 'Topic2')
|
||||
topic_3 = topics.create!(name: 'Topic3')
|
||||
topic_4 = topics.create!(name: 'Topic4')
|
||||
|
||||
project_topics.create!(project_id: project_1.id, topic_id: topic_1.id)
|
||||
project_topics.create!(project_id: project_1.id, topic_id: topic_3.id)
|
||||
project_topics.create!(project_id: project_2.id, topic_id: topic_3.id)
|
||||
project_topics.create!(project_id: project_1.id, topic_id: topic_4.id)
|
||||
project_topics.create!(project_id: project_2.id, topic_id: topic_4.id)
|
||||
project_topics.create!(project_id: project_3.id, topic_id: topic_4.id)
|
||||
|
||||
subject.perform(topic_1.id, topic_4.id)
|
||||
|
||||
expect(topic_1.reload.total_projects_count).to eq(1)
|
||||
expect(topic_2.reload.total_projects_count).to eq(0)
|
||||
expect(topic_3.reload.total_projects_count).to eq(2)
|
||||
expect(topic_4.reload.total_projects_count).to eq(3)
|
||||
end
|
||||
end
|
|
@ -32,6 +32,10 @@ RSpec.describe Gitlab::PerformanceBar::Stats do
|
|||
.with({ duration_ms: 23.709, filename: 'lib/gitlab/gitaly_client/commit_service.rb',
|
||||
method_path: 'lib/gitlab/gitaly_client/commit_service.rb:each',
|
||||
count: 1, request_id: 'foo', query_type: 'gitaly' })
|
||||
expect(logger).to receive(:info)
|
||||
.with({ duration_ms: 6.678, filename: 'lib/gitlab/gitaly_client/commit_service.rb',
|
||||
method_path: 'lib/gitlab/gitaly_client/commit_service.rb:call_find_commit',
|
||||
count: 1, request_id: 'foo', query_type: 'gitaly' })
|
||||
expect(logger).to receive(:info)
|
||||
.with({ duration_ms: 0.155, filename: 'lib/feature.rb',
|
||||
method_path: 'lib/feature.rb:enabled?',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!('schedule_populate_topics_total_projects_count_cache')
|
||||
|
||||
RSpec.describe SchedulePopulateTopicsTotalProjectsCountCache do
|
||||
let(:topics) { table(:topics) }
|
||||
let!(:topic_1) { topics.create!(name: 'Topic1') }
|
||||
let!(:topic_2) { topics.create!(name: 'Topic2') }
|
||||
let!(:topic_3) { topics.create!(name: 'Topic3') }
|
||||
|
||||
describe '#up' do
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
end
|
||||
|
||||
it 'schedules BackfillProjectsWithCoverage background jobs', :aggregate_failures do
|
||||
Sidekiq::Testing.fake! do
|
||||
freeze_time do
|
||||
migrate!
|
||||
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, topic_1.id, topic_2.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, topic_3.id, topic_3.id)
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserFeatures, :migration do
|
||||
let(:db) { described_class.new }
|
||||
let(:pg_class) { table(:pg_class) }
|
||||
let(:pg_index) { table(:pg_index) }
|
||||
let(:async_indexes) { table(:postgres_async_indexes) }
|
||||
|
||||
it "recreates index" do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be false
|
||||
expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be true
|
||||
expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be false
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be true
|
||||
expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be false
|
||||
expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be true
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,12 +3,18 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Topic do
|
||||
let_it_be(:topic, reload: true) { create(:topic) }
|
||||
let_it_be(:topic, reload: true) { create(:topic, name: 'topic') }
|
||||
|
||||
subject { topic }
|
||||
|
||||
it { expect(subject).to be_valid }
|
||||
|
||||
describe 'modules' do
|
||||
subject { described_class }
|
||||
|
||||
it { is_expected.to include_module(Avatarable) }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to have_many(:project_topics) }
|
||||
it { is_expected.to have_many(:projects) }
|
||||
|
@ -20,4 +26,74 @@ RSpec.describe Projects::Topic do
|
|||
it { is_expected.to validate_length_of(:name).is_at_most(255) }
|
||||
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
describe 'order_by_total_projects_count' do
|
||||
let!(:topic1) { create(:topic, name: 'topicB') }
|
||||
let!(:topic2) { create(:topic, name: 'topicC') }
|
||||
let!(:topic3) { create(:topic, name: 'topicA') }
|
||||
let!(:project1) { create(:project, topic_list: 'topicC, topicA, topicB') }
|
||||
let!(:project2) { create(:project, topic_list: 'topicC, topicA') }
|
||||
let!(:project3) { create(:project, topic_list: 'topicC') }
|
||||
|
||||
it 'sorts topics by total_projects_count' do
|
||||
topics = described_class.order_by_total_projects_count
|
||||
|
||||
expect(topics.map(&:name)).to eq(%w[topicC topicA topicB topic])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'reorder_by_similarity' do
|
||||
let!(:topic1) { create(:topic, name: 'my-topic') }
|
||||
let!(:topic2) { create(:topic, name: 'other') }
|
||||
let!(:topic3) { create(:topic, name: 'topic2') }
|
||||
|
||||
it 'sorts topics by similarity' do
|
||||
topics = described_class.reorder_by_similarity('topic')
|
||||
|
||||
expect(topics.map(&:name)).to eq(%w[topic my-topic topic2 other])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#search' do
|
||||
it 'returns topics with a matching name' do
|
||||
expect(described_class.search(topic.name)).to eq([topic])
|
||||
end
|
||||
|
||||
it 'returns topics with a partially matching name' do
|
||||
expect(described_class.search(topic.name[0..2])).to eq([topic])
|
||||
end
|
||||
|
||||
it 'returns topics with a matching name regardless of the casing' do
|
||||
expect(described_class.search(topic.name.upcase)).to eq([topic])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#avatar_type' do
|
||||
it "is true if avatar is image" do
|
||||
topic.update_attribute(:avatar, 'uploads/avatar.png')
|
||||
expect(topic.avatar_type).to be_truthy
|
||||
end
|
||||
|
||||
it "is false if avatar is html page" do
|
||||
topic.update_attribute(:avatar, 'uploads/avatar.html')
|
||||
topic.avatar_type
|
||||
|
||||
expect(topic.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#avatar_url' do
|
||||
context 'when avatar file is uploaded' do
|
||||
before do
|
||||
topic.update!(avatar: fixture_file_upload("spec/fixtures/dk.png"))
|
||||
end
|
||||
|
||||
it 'shows correct avatar url' do
|
||||
expect(topic.avatar_url).to eq(topic.avatar.url)
|
||||
expect(topic.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, topic.avatar.url].join)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,6 +58,15 @@ RSpec.describe 'layouts/nav/sidebar/_admin' do
|
|||
it_behaves_like 'page has active sub tab', 'Users'
|
||||
end
|
||||
|
||||
context 'on topics' do
|
||||
before do
|
||||
allow(controller).to receive(:controller_name).and_return('admin/topics')
|
||||
end
|
||||
|
||||
it_behaves_like 'page has active tab', 'Overview'
|
||||
it_behaves_like 'page has active sub tab', 'Topics'
|
||||
end
|
||||
|
||||
context 'on messages' do
|
||||
before do
|
||||
allow(controller).to receive(:controller_name).and_return('broadcast_messages')
|
||||
|
|
|
@ -20,7 +20,7 @@ module Deprecations
|
|||
YAML.load_file(file)
|
||||
end
|
||||
|
||||
deprecations = VersionSorter.rsort(deprecations) { |d| d["removal_milestone"] }
|
||||
deprecations = VersionSorter.sort(deprecations) { |d| d["removal_milestone"] }
|
||||
|
||||
milestones = deprecations.map { |d| d["removal_milestone"] }.uniq
|
||||
|
||||
|
|
Loading…
Reference in New Issue