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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-10 21:10:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-10 21:10:41 +0300
commit7f04cb580bc7895145fc1df51907582f80adbbca (patch)
tree31eb03182fcd84002895e3befb3d453bbcf048b4
parentbd5eb9f0201cf39ecfb0e754787a2297d5fdf051 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_manual_todo.yml3
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue26
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/helpers/invite_members_helper.rb4
-rw-r--r--app/helpers/namespaces_helper.rb9
-rw-r--r--app/models/integrations/asana.rb109
-rw-r--r--app/models/integrations/assembla.rb38
-rw-r--r--app/models/integrations/bamboo.rb183
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/asana_service.rb107
-rw-r--r--app/models/project_services/assembla_service.rb36
-rw-r--r--app/models/project_services/bamboo_service.rb181
-rw-r--r--app/models/project_services/chat_notification_service.rb2
-rw-r--r--app/models/service.rb51
-rw-r--r--app/presenters/project_presenter.rb2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_terms_of_service_notice.html.haml12
-rw-r--r--app/views/groups/_invite_members_modal.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml4
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml3
-rw-r--r--app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml11
-rw-r--r--app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml4
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label.html.haml21
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml16
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml2
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml15
-rw-r--r--app/views/shared/nav/_sidebar.html.haml2
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml4
-rw-r--r--changelogs/unreleased/329862-fix-project-path-generation.yml5
-rw-r--r--changelogs/unreleased/ar-collapsible-shortcut.yml5
-rw-r--r--changelogs/unreleased/change_invite_banner_button_to_open_modal.yml5
-rw-r--r--changelogs/unreleased/fix-slack-label-filter-behavior-blank.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/metrics/counts_28d/20210216181508_i_quickactions_approve_monthly.yml3
-rw-r--r--config/metrics/counts_7d/20210216181506_i_quickactions_approve_weekly.yml5
-rw-r--r--config/metrics/schema.json3
-rw-r--r--doc/administration/pages/index.md20
-rw-r--r--doc/development/usage_ping/metrics_dictionary.md1
-rw-r--r--doc/topics/git/git_rebase.md2
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/img/branching.pngbin12744 -> 0 bytes
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md137
-rw-r--r--doc/user/application_security/policies/index.md26
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/project/deploy_keys/index.md2
-rw-r--r--doc/user/project/img/protected_branches_list_v12_3.pngbin10208 -> 0 bytes
-rw-r--r--doc/user/project/img/protected_branches_page_v12_3.pngbin16336 -> 0 bytes
-rw-r--r--doc/user/project/import/bitbucket_server.md6
-rw-r--r--doc/user/project/import/gitea.md10
-rw-r--r--doc/user/project/import/jira.md2
-rw-r--r--doc/user/project/import/manifest.md12
-rw-r--r--doc/user/project/import/svn.md2
-rw-r--r--doc/user/project/import/tfvc.md14
-rw-r--r--doc/user/project/members/img/add_user_give_permissions_v13_8.pngbin69132 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.pngbin35191 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_imported_members_v13_9.pngbin58569 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_list_members_v13_9.pngbin48350 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_search_people_v13_8.pngbin28335 -> 0 bytes
-rw-r--r--doc/user/project/members/index.md123
-rw-r--r--doc/user/project/members/share_project_with_groups.md2
-rw-r--r--doc/user/project/protected_branches.md53
-rw-r--r--lib/api/helpers/services_helpers.rb6
-rw-r--r--lib/gitlab/integrations/sti_type.rb57
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric.rb13
-rw-r--r--lib/gitlab/usage/metrics/instrumentations/redis_hll_metric.rb2
-rw-r--r--lib/gitlab/usage_data.rb14
-rw-r--r--lib/sidebars/menu.rb18
-rw-r--r--lib/sidebars/menu_item.rb4
-rw-r--r--lib/sidebars/nil_menu_item.rb16
-rw-r--r--lib/sidebars/projects/menus/analytics_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/packages_registries_menu.rb2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--spec/features/groups/members/manage_members_spec.rb61
-rw-r--r--spec/features/projects/integrations/user_activates_asana_spec.rb (renamed from spec/features/projects/services/user_activates_asana_spec.rb)0
-rw-r--r--spec/features/projects/integrations/user_activates_assembla_spec.rb (renamed from spec/features/projects/services/user_activates_assembla_spec.rb)0
-rw-r--r--spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb (renamed from spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb)0
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js9
-rw-r--r--spec/helpers/invite_members_helper_spec.rb43
-rw-r--r--spec/helpers/namespaces_helper_spec.rb28
-rw-r--r--spec/lib/gitlab/integrations/sti_type_spec.rb116
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb15
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb5
-rw-r--r--spec/lib/sidebars/menu_spec.rb57
-rw-r--r--spec/lib/sidebars/projects/menus/analytics_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb48
-rw-r--r--spec/lib/sidebars/projects/menus/confluence_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/hidden_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/projects/menus/labels_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/operations_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb106
-rw-r--r--spec/lib/sidebars/projects/menus/project_information_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/projects/menus/wiki_menu_spec.rb2
-rw-r--r--spec/models/integrations/asana_spec.rb (renamed from spec/models/project_services/asana_service_spec.rb)18
-rw-r--r--spec/models/integrations/assembla_spec.rb (renamed from spec/models/project_services/assembla_service_spec.rb)2
-rw-r--r--spec/models/integrations/bamboo_spec.rb (renamed from spec/models/project_services/bamboo_service_spec.rb)2
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb18
-rw-r--r--spec/models/service_spec.rb21
-rw-r--r--spec/services/admin/propagate_service_template_spec.rb2
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb2
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb71
-rw-r--r--spec/views/groups/show.html.haml_spec.rb118
-rw-r--r--yarn.lock25
109 files changed, 1474 insertions, 822 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 7559717ae60..90ec92d0cde 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -1673,9 +1673,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_repository_storage_move.rb'
- 'app/models/project_services/alerts_service.rb'
- 'app/models/project_services/alerts_service_data.rb'
- - 'app/models/project_services/asana_service.rb'
- - 'app/models/project_services/assembla_service.rb'
- - 'app/models/project_services/bamboo_service.rb'
- 'app/models/project_services/bugzilla_service.rb'
- 'app/models/project_services/buildkite_service.rb'
- 'app/models/project_services/builds_email_service.rb'
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
index 747cea6a46e..402d9a07c53 100644
--- a/app/assets/javascripts/groups/components/invite_members_banner.vue
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -1,5 +1,6 @@
<script>
import { GlBanner } from '@gitlab/ui';
+import eventHub from '~/invite_members/event_hub';
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -11,7 +12,7 @@ export default {
GlBanner,
},
mixins: [trackingMixin],
- inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'],
+ inject: ['svgPath', 'isDismissedKey', 'trackLabel'],
data() {
return {
isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
@@ -20,11 +21,6 @@ export default {
},
};
},
- created() {
- this.$nextTick(() => {
- this.addTrackingAttributesToButton();
- });
- },
mounted() {
this.trackOnShow();
},
@@ -39,15 +35,12 @@ export default {
if (!this.isDismissed) this.track(this.$options.displayEvent);
});
},
- addTrackingAttributesToButton() {
- if (this.$refs.banner === undefined) return;
-
- const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`);
-
- if (button) {
- button.setAttribute('data-track-event', this.$options.buttonClickEvent);
- button.setAttribute('data-track-label', this.trackLabel);
- }
+ openModal() {
+ eventHub.$emit('openModal', {
+ inviteeType: 'members',
+ source: this.$options.openModalSource,
+ });
+ this.track(this.$options.buttonClickEvent);
},
},
i18n: {
@@ -59,6 +52,7 @@ export default {
},
displayEvent: 'invite_members_banner_displayed',
buttonClickEvent: 'invite_members_banner_button_clicked',
+ openModalSource: 'invite_members_banner',
dismissEvent: 'invite_members_banner_dismissed',
};
</script>
@@ -70,8 +64,8 @@ export default {
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:svg-path="svgPath"
- :button-link="inviteMembersPath"
@close="handleClose"
+ @primary="openModal"
>
<p>{{ $options.i18n.body }}</p>
</gl-banner>
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 9e75985c130..2aec0617b5a 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -3,6 +3,7 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
@@ -24,4 +25,5 @@ export default function initGroupDetails(actionName = 'show') {
new ProjectsList();
initInviteMembersBanner();
+ initInviteMembersModal();
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 01cf0beea3a..d343ba700ab 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -63,6 +63,9 @@ export default {
'\n',
);
},
+ mdCollapsibleSection() {
+ return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n');
+ },
isMac() {
// Accessing properties using ?. to allow tests to use
// this component without setting up window.gl.client.
@@ -245,6 +248,13 @@ export default {
icon="list-task"
/>
<toolbar-button
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ />
+ <toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index acd343da338..889c058cb21 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -3,10 +3,6 @@
module InviteMembersHelper
include Gitlab::Utils::StrongMemoize
- def can_invite_members_for_group?(group)
- Feature.enabled?(:invite_members_group_modal, group) && can?(current_user, :admin_group_member, group)
- end
-
def can_invite_members_for_project?(project)
Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index a4521541bf9..cf5fdc091af 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -83,6 +83,15 @@ module NamespacesHelper
}
end
+ def cascading_namespace_setting_locked?(attribute, group, **args)
+ return false if group.nil?
+
+ method_name = "#{attribute}_locked?"
+ return false unless group.namespace_settings.respond_to?(method_name)
+
+ group.namespace_settings.public_send(method_name, **args) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
private
# Many importers create a temporary Group, so use the real
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
new file mode 100644
index 00000000000..9c1975a6cff
--- /dev/null
+++ b/app/models/integrations/asana.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'asana'
+
+module Integrations
+ class Asana < Service
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :api_key, :restrict_to_branch
+ validates :api_key, presence: true, if: :activated?
+
+ def title
+ 'Asana'
+ end
+
+ def description
+ s_('AsanaService|Add commit messages as comments to Asana tasks.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'asana'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_key',
+ title: 'API key',
+ help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
+ # Example Personal Access Token from Asana docs
+ placeholder: '0/68a9e79b868c6789e79a124c30b0',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ title: 'Restrict to branch (optional)',
+ help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
+ }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def client
+ @_client ||= begin
+ ::Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ # check the branch restriction is poplulated and branch is not included
+ branch = Gitlab::Git.ref_name(data[:ref])
+ branch_restriction = restrict_to_branch.to_s
+ if branch_restriction.present? && branch_restriction.index(branch).nil?
+ return
+ end
+
+ user = data[:user_name]
+ project_name = project.full_name
+
+ data[:commits].each do |commit|
+ push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
+ check_commit(commit[:message], push_msg)
+ end
+ end
+
+ def check_commit(message, push_msg)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/{project_gid}/{task_gid}
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = ::Asana::Resources::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue StandardError => e
+ log_error(e.message)
+ next
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
new file mode 100644
index 00000000000..5307a8f0108
--- /dev/null
+++ b/app/models/integrations/assembla.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Assembla < Service
+ prop_accessor :token, :subdomain
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Assembla'
+ end
+
+ def description
+ _('Manage projects.')
+ end
+
+ def self.to_param
+ 'assembla'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'subdomain', placeholder: '' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
+ Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ end
+ end
+end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
new file mode 100644
index 00000000000..82111c7322e
--- /dev/null
+++ b/app/models/integrations/bamboo.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Bamboo < CiService
+ include ActionView::Helpers::UrlHelper
+ include ReactiveService
+
+ prop_accessor :bamboo_url, :build_key, :username, :password
+
+ validates :bamboo_url, presence: true, public_url: true, if: :activated?
+ validates :build_key, presence: true, if: :activated?
+ validates :username,
+ presence: true,
+ if: ->(service) { service.activated? && service.password }
+ validates :password,
+ presence: true,
+ if: ->(service) { service.activated? && service.username }
+
+ attr_accessor :response
+
+ after_save :compose_service_hook, if: :activated?
+ before_update :reset_password
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.save
+ end
+
+ def reset_password
+ if bamboo_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
+ def title
+ s_('BambooService|Atlassian Bamboo')
+ end
+
+ def description
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'bamboo'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'bamboo_url',
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('https://bamboo.example.com'),
+ help: s_('BambooService|Bamboo service root URL.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_key',
+ placeholder: s_('KEY'),
+ help: s_('BambooService|Bamboo build plan key.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ help: s_('BambooService|The user with API access to the Bamboo server.')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
+ ]
+ end
+
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
+ end
+
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action", { buildKey: build_key })
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
+
+ private
+
+ def get_build_result(response)
+ return if response&.code != 200
+
+ # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
+ result = response.dig('results', 'results', 'result')
+
+ # In case of multiple results, arbitrarily assume the last one is the most relevant.
+ return result.last if result.is_a?(Array)
+
+ result
+ end
+
+ def read_build_page(response)
+ result = get_build_result(response)
+ key =
+ if result.blank?
+ # If actual build link can't be determined, send user to build summary page.
+ build_key
+ else
+ # If actual build link is available, go to build result page.
+ result.dig('planResultKey', 'key')
+ end
+
+ build_url("browse/#{key}")
+ end
+
+ def read_commit_status(response)
+ return :error unless response && (response.code == 200 || response.code == 404)
+
+ result = get_build_result(response)
+ status =
+ if result.blank?
+ 'Pending'
+ else
+ result.dig('buildState')
+ end
+
+ return :error unless status.present?
+
+ if status.include?('Success')
+ 'success'
+ elsif status.include?('Failed')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
+ def try_get_path(path, query_params = {})
+ params = build_get_params(query_params)
+ params[:extra_log_info] = { project_id: project_id }
+
+ Gitlab::HTTP.try_get(build_url(path), params)
+ end
+
+ def get_path(path, query_params = {})
+ Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
+ end
+
+ def build_url(path)
+ Gitlab::Utils.append_path(bamboo_url, path)
+ end
+
+ def build_get_params(query_params)
+ params = { verify: false, query: query_params }
+ return params if username.blank? && password.blank?
+
+ query_params[:os_authType] = 'basic'
+ params[:basic_auth] = basic_auth
+ params
+ end
+
+ def basic_auth
+ { username: username, password: password }
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index af203d90e73..af7ddccf5e1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -149,7 +149,10 @@ class Project < ApplicationRecord
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards
- # Project services
+ # Project integrations
+ has_one :asana_service, class_name: 'Integrations::Asana'
+ has_one :assembla_service, class_name: 'Integrations::Assembla'
+ has_one :bamboo_service, class_name: 'Integrations::Bamboo'
has_one :campfire_service
has_one :datadog_service
has_one :discord_service
@@ -160,14 +163,11 @@ class Project < ApplicationRecord
has_one :irker_service
has_one :pivotaltracker_service
has_one :flowdock_service
- has_one :assembla_service
- has_one :asana_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
has_one :slack_service
has_one :buildkite_service
- has_one :bamboo_service
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
@@ -2603,7 +2603,7 @@ class Project < ApplicationRecord
end
def build_service(name)
- "#{name}_service".classify.constantize.new(project_id: id)
+ Service.service_name_to_model(name).new(project_id: id)
end
def services_templates
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
deleted file mode 100644
index 7e11e43170c..00000000000
--- a/app/models/project_services/asana_service.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'asana'
-
-class AsanaService < Service
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :api_key, :restrict_to_branch
- validates :api_key, presence: true, if: :activated?
-
- def title
- 'Asana'
- end
-
- def description
- s_('AsanaService|Add commit messages as comments to Asana tasks.')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
- s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'asana'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'api_key',
- title: 'API key',
- help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
- # Example Personal Access Token from Asana docs
- placeholder: '0/68a9e79b868c6789e79a124c30b0',
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- title: 'Restrict to branch (optional)',
- help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
- }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def client
- @_client ||= begin
- Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
- end
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- # check the branch restriction is poplulated and branch is not included
- branch = Gitlab::Git.ref_name(data[:ref])
- branch_restriction = restrict_to_branch.to_s
- if branch_restriction.present? && branch_restriction.index(branch).nil?
- return
- end
-
- user = data[:user_name]
- project_name = project.full_name
-
- data[:commits].each do |commit|
- push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
- check_commit(commit[:message], push_msg)
- end
- end
-
- def check_commit(message, push_msg)
- # matches either:
- # - #1234
- # - https://app.asana.com/0/{project_gid}/{task_gid}
- # optionally preceded with:
- # - fix/ed/es/ing
- # - close/s/d
- # - closing
- issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
-
- message.scan(issue_finder).each do |tuple|
- # tuple will be
- # [ 'fix', 'id_from_url', 'id_from_pound' ]
- taskid = tuple[2] || tuple[1]
-
- begin
- task = Asana::Resources::Task.find_by_id(client, taskid)
- task.add_comment(text: "#{push_msg} #{message}")
-
- if tuple[0]
- task.update(completed: true)
- end
- rescue StandardError => e
- log_error(e.message)
- next
- end
- end
- end
-end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
deleted file mode 100644
index 8845fb99605..00000000000
--- a/app/models/project_services/assembla_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-class AssemblaService < Service
- prop_accessor :token, :subdomain
- validates :token, presence: true, if: :activated?
-
- def title
- 'Assembla'
- end
-
- def description
- _('Manage projects.')
- end
-
- def self.to_param
- 'assembla'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'subdomain', placeholder: '' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
- end
-end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
deleted file mode 100644
index 60f53bd4a7d..00000000000
--- a/app/models/project_services/bamboo_service.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-# frozen_string_literal: true
-
-class BambooService < CiService
- include ActionView::Helpers::UrlHelper
- include ReactiveService
-
- prop_accessor :bamboo_url, :build_key, :username, :password
-
- validates :bamboo_url, presence: true, public_url: true, if: :activated?
- validates :build_key, presence: true, if: :activated?
- validates :username,
- presence: true,
- if: ->(service) { service.activated? && service.password }
- validates :password,
- presence: true,
- if: ->(service) { service.activated? && service.username }
-
- attr_accessor :response
-
- after_save :compose_service_hook, if: :activated?
- before_update :reset_password
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.save
- end
-
- def reset_password
- if bamboo_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
- def title
- s_('BambooService|Atlassian Bamboo')
- end
-
- def description
- s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
- s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'bamboo'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'bamboo_url',
- title: s_('BambooService|Bamboo URL'),
- placeholder: s_('https://bamboo.example.com'),
- help: s_('BambooService|Bamboo service root URL.'),
- required: true
- },
- {
- type: 'text',
- name: 'build_key',
- placeholder: s_('KEY'),
- help: s_('BambooService|Bamboo build plan key.'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('BambooService|The user with API access to the Bamboo server.')
- },
- {
- type: 'password',
- name: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
- }
- ]
- end
-
- def build_page(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
- end
-
- def commit_status(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action", { buildKey: build_key })
- end
-
- def calculate_reactive_cache(sha, ref)
- response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
-
- { build_page: read_build_page(response), commit_status: read_commit_status(response) }
- end
-
- private
-
- def get_build_result(response)
- return if response&.code != 200
-
- # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
- result = response.dig('results', 'results', 'result')
-
- # In case of multiple results, arbitrarily assume the last one is the most relevant.
- return result.last if result.is_a?(Array)
-
- result
- end
-
- def read_build_page(response)
- result = get_build_result(response)
- key =
- if result.blank?
- # If actual build link can't be determined, send user to build summary page.
- build_key
- else
- # If actual build link is available, go to build result page.
- result.dig('planResultKey', 'key')
- end
-
- build_url("browse/#{key}")
- end
-
- def read_commit_status(response)
- return :error unless response && (response.code == 200 || response.code == 404)
-
- result = get_build_result(response)
- status =
- if result.blank?
- 'Pending'
- else
- result.dig('buildState')
- end
-
- return :error unless status.present?
-
- if status.include?('Success')
- 'success'
- elsif status.include?('Failed')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
- end
-
- def try_get_path(path, query_params = {})
- params = build_get_params(query_params)
- params[:extra_log_info] = { project_id: project_id }
-
- Gitlab::HTTP.try_get(build_url(path), params)
- end
-
- def get_path(path, query_params = {})
- Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
- end
-
- def build_url(path)
- Gitlab::Utils.append_path(bamboo_url, path)
- end
-
- def build_get_params(query_params)
- params = { verify: false, query: query_params }
- return params if username.blank? && password.blank?
-
- query_params[:os_authType] = 'basic'
- params[:basic_auth] = basic_auth
- params
- end
-
- def basic_auth
- { username: username, password: password }
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index b2881337d01..bc92480755b 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -30,7 +30,7 @@ class ChatNotificationService < Service
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, public_url: true, if: :activated?
- validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_nil: true
+ validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
if properties.nil?
diff --git a/app/models/service.rb b/app/models/service.rb
index 4d52056365e..940caee8d65 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -28,6 +28,8 @@ class Service < ApplicationRecord
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :type, Gitlab::Integrations::StiType.new
+
default_value_for :active, false
default_value_for :alert_events, true
default_value_for :category, 'common'
@@ -164,22 +166,23 @@ class Service < ApplicationRecord
end
def self.create_nonexistent_templates
- nonexistent_services = list_nonexistent_services_for(for_template)
+ nonexistent_services = build_nonexistent_services_for(for_template)
return if nonexistent_services.empty?
# Create within a transaction to perform the lowest possible SQL queries.
transaction do
- nonexistent_services.each do |service_type|
- service_type.constantize.create(template: true)
+ nonexistent_services.each do |service|
+ service.template = true
+ service.save
end
end
end
private_class_method :create_nonexistent_templates
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
- if name.in?(available_services_names(include_project_specific: false))
- "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id)
- end
+ return unless name.in?(available_services_names(include_project_specific: false))
+
+ service_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope)
@@ -187,19 +190,23 @@ class Service < ApplicationRecord
end
def self.build_nonexistent_services_for(scope)
- list_nonexistent_services_for(scope).map do |service_type|
- service_type.constantize.new
+ nonexistent_services_types_for(scope).map do |service_type|
+ service_type_to_model(service_type).new
end
end
private_class_method :build_nonexistent_services_for
- def self.list_nonexistent_services_for(scope)
+ # Returns a list of service types that do not exist in the given scope.
+ # Example: ["AsanaService", ...]
+ def self.nonexistent_services_types_for(scope)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_services_types(include_project_specific: false) - scope.map(&:type)
end
- private_class_method :list_nonexistent_services_for
+ private_class_method :nonexistent_services_types_for
+ # Returns a list of available service names.
+ # Example: ["asana", ...]
def self.available_services_names(include_project_specific: true, include_dev: true)
service_names = services_names
service_names += project_specific_services_names if include_project_specific
@@ -222,12 +229,34 @@ class Service < ApplicationRecord
PROJECT_SPECIFIC_SERVICE_NAMES
end
+ # Returns a list of available service types.
+ # Example: ["AsanaService", ...]
def self.available_services_types(include_project_specific: true, include_dev: true)
available_services_names(include_project_specific: include_project_specific, include_dev: include_dev).map do |service_name|
- "#{service_name}_service".camelize
+ service_name_to_type(service_name)
end
end
+ # Returns the model for the given service name.
+ # Example: "asana" => Integrations::Asana
+ def self.service_name_to_model(name)
+ type = service_name_to_type(name)
+ service_type_to_model(type)
+ end
+
+ # Returns the STI type for the given service name.
+ # Example: "asana" => "AsanaService"
+ def self.service_name_to_type(name)
+ "#{name}_service".camelize
+ end
+
+ # Returns the model for the given STI type.
+ # Example: "AsanaService" => Integrations::Asana
+ def self.service_type_to_model(type)
+ Gitlab::Integrations::StiType.new.cast(type).constantize
+ end
+ private_class_method :service_type_to_model
+
def self.build_from_integration(integration, project_id: nil, group_id: nil)
service = integration.dup
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 5e53dbf33d8..a03137cd0d9 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -210,7 +210,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
- empty_repo? ? nil : project_commits_path(project, repository.root_ref))
+ empty_repo? ? nil : project_commits_path(project, default_branch_or_main))
end
def branches_anchor_data
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 2fc89f18de6..56f74916d8f 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -37,6 +37,6 @@
= recaptcha_tags
.submit-container
= f.submit button_text, class: 'btn gl-button btn-confirm', data: { qa_selector: 'new_user_register_button' }
- = render 'devise/shared/terms_of_service_notice'
+ = render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom
= render 'devise/shared/signup_omniauth_providers'
diff --git a/app/views/devise/shared/_terms_of_service_notice.html.haml b/app/views/devise/shared/_terms_of_service_notice.html.haml
index 46b043b2831..75d567a03fd 100644
--- a/app/views/devise/shared/_terms_of_service_notice.html.haml
+++ b/app/views/devise/shared/_terms_of_service_notice.html.haml
@@ -1,5 +1,9 @@
-- company_name = Gitlab.com? ? 'GitLab' : ''
+- return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
-- if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
- %p.gl-text-gray-500.gl-mt-5.gl-mb-0
- = html_escape(_("By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}")) % { linkStart: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, linkEnd: '</a>'.html_safe, company_name: company_name }
+%p.gl-text-gray-500.gl-mt-5.gl-mb-0
+ - if Gitlab.dev_env_or_com?
+ = html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
+ link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
+ - else
+ = html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
+ link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index ba6dfcb70ff..69ed94e99cc 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -1,4 +1,4 @@
-- if can_invite_members_for_group?(group)
+- if can?(current_user, :admin_group_member, group)
.js-invite-members-modal{ data: { id: group.id,
name: group.name,
is_project: 'false',
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index fc67057f417..cea4c533ae3 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -15,13 +15,13 @@
= _('Group members')
%p
= html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- - if can_invite_members_for_group?(@group)
+ - if Feature.enabled?(:invite_members_group_modal, @group)
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- - if can_manage_members && !can_invite_members_for_group?(@group)
+ - if can_manage_members && Feature.disabled?(:invite_members_group_modal, @group)
%hr.gl-mt-4
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a1557cda071..9f7f0a08df5 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -12,6 +12,7 @@
is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
track_label: 'invite_members_banner',
invite_members_path: group_group_members_path(@group) } }
+ = render 'groups/invite_members_modal', group: @group
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 73f3d2a8fcd..033ed69da41 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -19,6 +19,9 @@
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a task list") })
+ = markdown_toolbar_button({ icon: "details-block",
+ data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" },
+ title: _("Add a collapsible section") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
index 1e9aa4ec5ff..ab4d8816ec9 100644
--- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -1,14 +1,17 @@
- attribute = local_assigns.fetch(:attribute, nil)
-- group = local_assigns.fetch(:group, nil)
- form = local_assigns.fetch(:form, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, s_('CascadingSettings|Subgroups cannot change this setting.'))
- return unless attribute && group && form && cascading_namespace_settings_enabled?
-- return if group.namespace_settings.public_send("#{attribute}_locked?")
+- return if setting_locked
- lock_attribute = "lock_#{attribute}"
.gl-form-checkbox.custom-control.custom-checkbox
= form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
= form.label lock_attribute, class: 'custom-control-label' do
- %span= s_('CascadingSettings|Enforce for all subgroups')
- %p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.')
+ %span
+ = yield.presence || s_('CascadingSettings|Enforce for all subgroups')
+ %p.help-text
+ = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
new file mode 100644
index 00000000000..4e3b6b2afc4
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
@@ -0,0 +1,4 @@
+%button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
+ type: 'button',
+ data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
+ = sprite_icon('lock', size: 16)
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
deleted file mode 100644
index 6596ce2bc73..00000000000
--- a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- attribute = local_assigns.fetch(:attribute, nil)
-- group = local_assigns.fetch(:group, nil)
-- form = local_assigns.fetch(:form, nil)
-- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
-- help_text = local_assigns.fetch(:help_text, nil)
-
-- return unless attribute && group && form && settings_path_helper
-
-- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?")
-
-= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
- %span.position-relative.gl-pr-6.gl-display-inline-flex
- = yield
- - if setting_locked
- %button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
- type: 'button',
- data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
- = sprite_icon('lock', size: 16)
- - if help_text
- %p.help-text
- = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml
new file mode 100644
index 00000000000..d27b3641637
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml
@@ -0,0 +1,16 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
+- form = local_assigns.fetch(:form, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, nil)
+
+- return unless attribute && form && settings_path_helper
+
+= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
+ = render 'shared/namespaces/cascading_settings/setting_label_container' do
+ = yield
+ - if setting_locked
+ = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns
+ - if help_text
+ %p.help-text
+ = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml
new file mode 100644
index 00000000000..7323295f1fe
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml
@@ -0,0 +1,2 @@
+%span.position-relative.gl-pr-6.gl-display-inline-flex
+ = yield
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml
new file mode 100644
index 00000000000..4a2ec9f30fd
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml
@@ -0,0 +1,15 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, nil)
+
+- return unless attribute && settings_path_helper
+
+%legend.h5.gl-border-none.gl-m-0
+ = render 'shared/namespaces/cascading_settings/setting_label_container' do
+ = yield
+ - if setting_locked
+ = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns
+- if help_text
+ %p.gl-text-gray-500
+ = help_text
diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml
index 8e388f2826d..552dcbfd6fd 100644
--- a/app/views/shared/nav/_sidebar.html.haml
+++ b/app/views/shared/nav/_sidebar.html.haml
@@ -11,5 +11,5 @@
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial
- = render partial: 'shared/nav/sidebar_hidden_menu_item', collection: sidebar.hidden_menu&.items
+ = render partial: 'shared/nav/sidebar_hidden_menu_item', collection: sidebar.hidden_menu&.renderable_items
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
index 789fb41064f..67c775d1a85 100644
--- a/app/views/shared/nav/_sidebar_menu.html.haml
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -13,7 +13,7 @@
%span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
= number_with_delimiter(sidebar_menu.pill_count)
- %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_items?) }
+ %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
= nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
= link_to sidebar_menu.link, **sidebar_menu.collapsed_container_html_options do
%strong.fly-out-top-item-name
@@ -24,4 +24,4 @@
- if sidebar_menu.has_items?
%li.divider.fly-out-top-item
- = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.items
+ = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
diff --git a/changelogs/unreleased/329862-fix-project-path-generation.yml b/changelogs/unreleased/329862-fix-project-path-generation.yml
new file mode 100644
index 00000000000..bfe621224db
--- /dev/null
+++ b/changelogs/unreleased/329862-fix-project-path-generation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a bug displaying project commit anchors
+merge_request: 61361
+author:
+type: fixed
diff --git a/changelogs/unreleased/ar-collapsible-shortcut.yml b/changelogs/unreleased/ar-collapsible-shortcut.yml
new file mode 100644
index 00000000000..3837d35434a
--- /dev/null
+++ b/changelogs/unreleased/ar-collapsible-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Add collapsible section shortcut to markdown controls
+merge_request: 54938
+author:
+type: added
diff --git a/changelogs/unreleased/change_invite_banner_button_to_open_modal.yml b/changelogs/unreleased/change_invite_banner_button_to_open_modal.yml
new file mode 100644
index 00000000000..0521d238944
--- /dev/null
+++ b/changelogs/unreleased/change_invite_banner_button_to_open_modal.yml
@@ -0,0 +1,5 @@
+---
+title: Replace invite banner button with modal trigger
+merge_request: 59260
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-slack-label-filter-behavior-blank.yml b/changelogs/unreleased/fix-slack-label-filter-behavior-blank.yml
new file mode 100644
index 00000000000..91e17ec7991
--- /dev/null
+++ b/changelogs/unreleased/fix-slack-label-filter-behavior-blank.yml
@@ -0,0 +1,5 @@
+---
+title: Fix slack label filter behavior blank
+merge_request: 61236
+author:
+type: fixed
diff --git a/config/application.rb b/config/application.rb
index 4d734c713fb..320cfbf824f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -47,6 +47,7 @@ module Gitlab
config.eager_load_paths.push(*%W[#{config.root}/lib
#{config.root}/app/models/badges
#{config.root}/app/models/hooks
+ #{config.root}/app/models/integrations
#{config.root}/app/models/members
#{config.root}/app/models/project_services
#{config.root}/app/graphql/resolvers/concerns
diff --git a/config/metrics/counts_28d/20210216181508_i_quickactions_approve_monthly.yml b/config/metrics/counts_28d/20210216181508_i_quickactions_approve_monthly.yml
index b1009642dcc..9b9fa1779c7 100644
--- a/config/metrics/counts_28d/20210216181508_i_quickactions_approve_monthly.yml
+++ b/config/metrics/counts_28d/20210216181508_i_quickactions_approve_monthly.yml
@@ -9,9 +9,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
+instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric'
distribution:
- ce
-- ee
+- ee
tier:
- free
- premium
diff --git a/config/metrics/counts_7d/20210216181506_i_quickactions_approve_weekly.yml b/config/metrics/counts_7d/20210216181506_i_quickactions_approve_weekly.yml
index f8c48ac20f9..3754b20fb6e 100644
--- a/config/metrics/counts_7d/20210216181506_i_quickactions_approve_weekly.yml
+++ b/config/metrics/counts_7d/20210216181506_i_quickactions_approve_weekly.yml
@@ -9,10 +9,11 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
+instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric'
distribution:
- ce
-- ee
+- ee
tier:
- free
- premium
-- ultimate \ No newline at end of file
+- ultimate
diff --git a/config/metrics/schema.json b/config/metrics/schema.json
index e9e3ac5ef40..66d2ee4ca03 100644
--- a/config/metrics/schema.json
+++ b/config/metrics/schema.json
@@ -43,6 +43,9 @@
"introduced_by_url": {
"type": ["string", "null"]
},
+ "extra": {
+ "type": "object"
+ },
"time_frame": {
"type": "string",
"enum": ["7d", "28d", "all", "none"]
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index a422efc0fcf..404e28c58c6 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -120,7 +120,7 @@ This example contains the following:
- `example.com`: The GitLab domain.
- `example.io`: The domain GitLab Pages is served from.
- `192.0.2.1`: The primary IP of your GitLab instance.
-- `192.0.2.2`: The secondary IP, which is dedicated to GitLab Pages.
+- `192.0.2.2`: The secondary IP, which is dedicated to GitLab Pages. It must be different than the primary IP.
NOTE:
You should not use the GitLab domain to serve user pages. For more information see the [security section](#security).
@@ -310,14 +310,12 @@ world. Custom domains are supported, but no TLS.
```ruby
pages_external_url "http://example.io"
- nginx['listen_addresses'] = ['192.0.2.1']
+ nginx['listen_addresses'] = ['192.0.2.1'] # The primary IP of the GitLab instance
pages_nginx['enable'] = false
- gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80']
+ gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80'] # The secondary IPs for the GitLab Pages daemon
```
- where `192.0.2.1` is the primary IP address that GitLab is listening to and
- `192.0.2.2` and `2001:db8::2` are the secondary IPs the GitLab Pages daemon
- listens on. If you don't have IPv6, you can omit the IPv6 address.
+ If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
@@ -342,17 +340,15 @@ world. Custom domains and TLS are supported.
```ruby
pages_external_url "https://example.io"
- nginx['listen_addresses'] = ['192.0.2.1']
+ nginx['listen_addresses'] = ['192.0.2.1'] # The primary IP of the GitLab instance
pages_nginx['enable'] = false
- gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80']
- gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001:db8::2]:443']
+ gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80'] # The secondary IPs for the GitLab Pages daemon
+ gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001:db8::2]:443'] # The secondary IPs for the GitLab Pages daemon
# Redirect pages from HTTP to HTTPS
gitlab_pages['redirect_http'] = true
```
- where `192.0.2.1` is the primary IP address that GitLab is listening to and
- `192.0.2.2` and `2001:db8::2` are the secondary IPs where the GitLab Pages daemon
- listens on. If you don't have IPv6, you can omit the IPv6 address.
+ If you don't have IPv6, you can omit the IPv6 address.
1. If you haven't named your certificate and key `example.io.crt` and `example.io.key` respectively,
then you need to also add the full paths as shown below:
diff --git a/doc/development/usage_ping/metrics_dictionary.md b/doc/development/usage_ping/metrics_dictionary.md
index fd7e0777076..93102bc6275 100644
--- a/doc/development/usage_ping/metrics_dictionary.md
+++ b/doc/development/usage_ping/metrics_dictionary.md
@@ -43,6 +43,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `milestone` | no | The milestone when the metric is introduced. |
| `milestone_removed` | no | The milestone when the metric is removed. |
| `introduced_by_url` | no | The URL to the Merge Request that introduced the metric. |
+| `extra` | no | `object`: extra information needed to calculate the metric value. |
| `skip_validation` | no | This should **not** be set. [Used for imported metrics until we review, update and make them valid](https://gitlab.com/groups/gitlab-org/-/epics/5425). |
### Metric statuses
diff --git a/doc/topics/git/git_rebase.md b/doc/topics/git/git_rebase.md
index bf77ba3272c..78303d24a20 100644
--- a/doc/topics/git/git_rebase.md
+++ b/doc/topics/git/git_rebase.md
@@ -175,7 +175,7 @@ a macOS's `ZSH` shell, and you want to **squash** all the three commits
Note that the steps for editing through the command line can be slightly
different depending on your operating system and the shell you're using.
-See [Numerous undo possibilities in Git](numerous_undo_possibilities_in_git/index.md#with-history-modification)
+See [Numerous undo possibilities in Git](numerous_undo_possibilities_in_git/index.md#undo-staged-local-changes-without-modifying-history)
for a deeper look into interactive rebase.
## Force-push
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/img/branching.png b/doc/topics/git/numerous_undo_possibilities_in_git/img/branching.png
deleted file mode 100644
index d8dc9fc8097..00000000000
--- a/doc/topics/git/numerous_undo_possibilities_in_git/img/branching.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 2426fa2f7cb..b151ddfff71 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -44,7 +44,7 @@ You can undo changes at any point in this workflow:
Until you push your changes to a remote repository, changes
you make in Git are only in your local development environment.
-### Undo unstaged local changes before you commit
+### Undo unstaged local changes
When you make a change, but have not yet staged it, you can undo your work.
@@ -82,7 +82,7 @@ When you make a change, but have not yet staged it, you can undo your work.
git reset --hard
```
-### Undo staged local changes before you commit
+### Undo staged local changes
If you added a file to staging, you can undo it.
@@ -132,104 +132,85 @@ If you added a file to staging, you can undo it.
### Quickly save local changes
-You are working on a feature when someone drops by with an urgent task. Because your
-feature is not complete, but you need to swap to another branch, you can use
-`git stash` to:
+If you want to change to another branch, you can use [`git stash`](https://www.git-scm.com/docs/git-stash).
-- Save what you have done.
-- Swap to another branch.
-- Commit, push, and test.
-- Return to the feature branch.
-- Run `git stash pop`.
-- Resume your work.
+1. From the branch where you want to save your work, type `git stash`.
+1. Swap to another branch (`git checkout <branchname>`).
+1. Commit, push, and test.
+1. Return to the branch where you want to resume your changes.
+1. Use `git stash list` to list all previously stashed commits.
+1. Run a version of `git stash`:
-The example above shows that discarding all changes is not always a preferred option.
-However, Git provides a way to save them for later, while resetting the repository to state without
-them. This is achieved by Git stashing command `git stash`, which in fact saves your
-current work and runs `git reset --hard`, but it also has various
-additional options like:
-
-- `git stash save`, which enables including temporary commit message, which helps you identify changes, among with other options
-- `git stash list`, which lists all previously stashed commits (yes, there can be more) that were not `pop`ed
-- `git stash pop`, which redoes previously stashed changes and removes them from stashed list
-- `git stash apply`, which redoes previously stashed changes, but keeps them on stashed list
+ - Use `git stash pop` to redo previously stashed changes and remove them from stashed list.
+ - Use `git stash apply` to redo previously stashed changes, but keep them on stashed list.
## Undo committed local changes
-After you commit, your changes are recorded by the version control system.
-Because you haven't pushed to your remote repository yet, your changes are
-still not public (or shared with other developers). At this point, undoing
-things is a lot easier, we have quite some workaround options. After you push
-your code, you have fewer options to troubleshoot your work.
+When you commit to your local repository (`git commit`), the version control system records
+your changes. Because you did not push to a remote repository yet, your changes are
+not public (or shared with other developers). At this point, you can undo your changes.
-### Without modifying history
+### Undo staged local changes without modifying history
-Through the development process some of the previously committed changes do not
-fit anymore in the end solution, or are source of the bugs. After you find the
-commit which triggered bug, or identify a faulty commit, you can
-revert it with `git revert commit-id`.
+You can revert a commit while retaining the commit history.
-This command inverts (swaps) the additions and
-deletions in that commit, so that it does not modify history. Retaining history
-can be helpful in future to notice that some changes have been tried
-unsuccessfully in the past.
+This example uses five commits `A`,`B`,`C`,`D`,`E`, which were committed in order: `A-B-C-D-E`.
+The commit you want to undo is `B`.
-In our example we assume there are commits `A`,`B`,`C`,`D`,`E` committed in this order: `A-B-C-D-E`,
-and `B` is the commit you want to undo. There are many different ways to identify commit
-`B` as bad. One of them is to pass a range to `git bisect` command. The provided range includes
-last known good commit (we assume `A`) and first known bad commit where the bug was detected (we assume `E`).
+1. Find the commit SHA of the commit you want to revert to. To look
+ through a log of commits, type `git log`.
+1. Choose an option and undo your changes:
-```shell
-git bisect A..E
-```
+ - To swap additions and deletions changes introduced by commit `B`:
-Bisect provides us with commit ID of the middle commit to test, and then guide us
-through the bisection process. You can read more about it [in official Git Tools](https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git)
-Our example results in commit `B`, which introduced the bug/error. We have
-these options to remove all or part of it from our repository:
+ ```shell
+ git revert <commit-B-SHA>
+ ```
-- Undo (swap additions and deletions) changes introduced by commit `B`:
+ - To undo changes on a single file or directory from commit `B`, but retain them in the staged state:
- ```shell
- git revert commit-B-id
- ```
+ ```shell
+ git checkout <commit-B-SHA> <file>
+ ```
-- Undo changes on a single file or directory from commit `B`, but retain them in the staged state:
+ - To undo changes on a single file or directory from commit `B`, but retain them in the unstaged state:
- ```shell
- git checkout commit-B-id <file>
- ```
+ ```shell
+ git reset <commit-B-SHA> <file>
+ ```
-- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state:
+#### Undo multiple committed changes
- ```shell
- git reset commit-B-id <file>
- ```
+You can recover from multiple commits. For example, if you have done commits `A-B-C-D`
+on your feature branch and then realize that `C` and `D` are wrong.
-- There is one command we also must not forget: **creating a new branch**
- from the point where changes are not applicable or where the development has hit a
- dead end. For example you have done commits `A-B-C-D` on your feature branch
- and then you figure `C` and `D` are wrong.
+To recover from multiple incorrect commits:
- At this point you either reset to `B`
- and do commit `F` (which causes problems with pushing and if forced pushed also with other developers)
- because the branch now looks `A-B-F`, which clashes with what other developers have locally (you will
- [change history](#with-history-modification)), or you checkout commit `B` create
- a new branch and do commit `F`. In the last case, everyone else can still do their work while you
- have your new way to get it right and merge it back in later. Alternatively, with GitLab,
- you can [cherry-pick](../../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- that commit into a new merge request.
+1. Check out the last correct commit. In this example, `B`.
- ![Create a new branch to avoid clashing](img/branching.png)
+ ```shell
+ git checkout <commit-B-SHA>
+ ```
- ```shell
- git checkout commit-B-id
- git checkout -b new-path-of-feature
- # Create <commit F>
- git commit -a
- ```
+1. Create a new branch.
+
+ ```shell
+ git checkout -b new-path-of-feature
+ ```
+
+1. Add, push, and commit your changes.
+
+The commits are now `A-B-C-D-E`.
+
+Alternatively, with GitLab,
+you can [cherry-pick](../../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
+that commit into a new merge request.
+
+NOTE:
+Another solution is to reset to `B` and commit `E`. However, this solution results in `A-B-E`,
+which clashes with what other developers have locally.
-### With history modification
+### Undo staged local changes with history modification
You can rewrite history in Git, but you should avoid it, because it can cause problems
when multiple developers are contributing to the same codebase.
diff --git a/doc/user/application_security/policies/index.md b/doc/user/application_security/policies/index.md
index c910ae6b662..92f0d6924b3 100644
--- a/doc/user/application_security/policies/index.md
+++ b/doc/user/application_security/policies/index.md
@@ -112,6 +112,16 @@ This rule enforces the defined actions whenever the pipeline runs for a selected
| `type` | `string` | `pipeline` | The rule's type. |
| `branches` | `array` of `string` | `*` or the branch's name | The branch the given policy applies to (supports wildcard). |
+### `schedule` rule type
+
+This rule enforces the defined actions and schedules a scan on the provided date/time.
+
+| Field | Type | Possible values | Description |
+|------------|------|-----------------|-------------|
+| `type` | `string` | `schedule` | The rule's type. |
+| `branches` | `array` of `string` | `*` or the branch's name | The branch the given policy applies to (supports wildcard). |
+| `cadence` | `string` | CRON expression (for example, `0 0 * * *`) | A whitespace-separated string containing five fields that represents the scheduled time. |
+
### `scan` action type
This action executes the selected `scan` with additional parameters when conditions for at least one
@@ -131,6 +141,9 @@ Note the following:
- Once you associate the site profile and scanner profile by name in the policy, it is not possible
to modify or delete them. If you want to modify them, you must first disable the policy by setting
the `active` flag to `false`.
+- When configuring policies with a scheduled DAST scan, the author of the commit in the security
+ policy project's repository must have access to the scanner and site profiles. Otherwise, the scan
+ is not scheduled successfully.
Here's an example:
@@ -148,13 +161,14 @@ scan_execution_policy:
- scan: dast
scanner_profile: Scanner Profile A
site_profile: Site Profile B
-- name: Enforce DAST in every pipeline in main branch
- description: This policy enforces pipeline configuration to have a job with DAST scan for main branch
+- name: Enforce DAST scan every 10 minutes
+ description: This policy enforces a DAST scan to run every 10 minutes
enabled: true
rules:
- - type: pipeline
+ - type: schedule
branches:
- main
+ cadence: */10 * * * *
actions:
- scan: dast
scanner_profile: Scanner Profile C
@@ -164,11 +178,7 @@ scan_execution_policy:
In this example, the DAST scan runs with the scanner profile `Scanner Profile A` and the site
profile `Site Profile B` for every pipeline executed on branches that match the
`release/*` wildcard (for example, branch name `release/v1.2.1`); and the DAST scan runs with
-the scanner profile `Scanner Profile C` and the site profile `Site Profile D` for every pipeline executed on `main` branch.
-
-NOTE:
-All scanner and site profiles must be configured and created for each project that is assigned to the selected Security Policy Project.
-If they are not created, the job will fail with the error message.
+the scanner profile `Scanner Profile C` and the site profile `Site Profile D` every 10 minutes.
## Security Policy project selection
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 0c257f2fb70..cbd5bf1553a 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1092,7 +1092,7 @@ Markdown is fine in GitLab.
</dd>
</dl>
-#### Details and summary
+#### Collapsible section
To see the second Markdown example rendered in HTML,
[view it in GitLab](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#details-and-summary).
diff --git a/doc/user/project/deploy_keys/index.md b/doc/user/project/deploy_keys/index.md
index bf082cca93a..45c42c78ab7 100644
--- a/doc/user/project/deploy_keys/index.md
+++ b/doc/user/project/deploy_keys/index.md
@@ -176,7 +176,7 @@ If the key is **privately accessible** and in use by other projects, it will be
If the owner of this deploy key doesn't have access to a [protected
branch](../protected_branches.md), then this deploy key doesn't have access to
the branch either. In addition to this, choosing the **No one** value in
-[the "Allowed to push" section](../protected_branches.md#configuring-protected-branches)
+[the "Allowed to push" section](../protected_branches.md#configure-a-protected-branch)
means that no users **and** no services using deploy keys can push to that selected branch.
Refer to [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/30769) for more information.
diff --git a/doc/user/project/img/protected_branches_list_v12_3.png b/doc/user/project/img/protected_branches_list_v12_3.png
deleted file mode 100644
index 995a294b85c..00000000000
--- a/doc/user/project/img/protected_branches_list_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/protected_branches_page_v12_3.png b/doc/user/project/img/protected_branches_page_v12_3.png
deleted file mode 100644
index 60aa3c4d251..00000000000
--- a/doc/user/project/img/protected_branches_page_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md
index 330ed0cb774..1e79107d76f 100644
--- a/doc/user/project/import/bitbucket_server.md
+++ b/doc/user/project/import/bitbucket_server.md
@@ -22,10 +22,8 @@ The Bitbucket importer can import:
- Pull requests (GitLab 11.2+)
- Pull request comments (GitLab 11.2+)
-When importing:
-
-- Repository public access is retained. If a repository is private in Bitbucket, it's created as
- private in GitLab as well.
+When importing, repository public access is retained. If a repository is private in Bitbucket, it's
+created as private in GitLab as well.
## Limitations
diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md
index 255d083be6f..41141902468 100644
--- a/doc/user/project/import/gitea.md
+++ b/doc/user/project/import/gitea.md
@@ -21,10 +21,8 @@ The Gitea importer can import:
- Milestones (GitLab 8.15+)
- Labels (GitLab 8.15+)
-When importing:
-
-- Repository public access is retained. If a repository is private in Gitea it's created as private
- in GitLab as well.
+When importing, repository public access is retained. If a repository is private in Gitea, it's
+created as private in GitLab as well.
## How it works
@@ -78,9 +76,9 @@ From there, you can see the import statuses of your Gitea repositories.
You also can:
- Import all your Gitea projects in one go by hitting **Import all projects** in
- the upper left corner
+ the upper left corner.
- Filter projects by name. If filter is applied, hitting **Import all projects**
- only imports matched projects
+ only imports matched projects.
![Gitea importer page](img/import_projects_from_gitea_importer_v12_3.png)
diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md
index 6b8c34fada6..4273f90c1e7 100644
--- a/doc/user/project/import/jira.md
+++ b/doc/user/project/import/jira.md
@@ -59,7 +59,7 @@ Importing Jira issues is done as an asynchronous background job, which
may result in delays based on import queues load, system load, or other factors.
Importing large projects may take several minutes depending on the size of the import.
-To import Jira issues to a GitLab project, follow these steps:
+To import Jira issues to a GitLab project:
1. On the **{issues}** **Issues** page, click **Import Issues** (**{import}**) **> Import from Jira**.
diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md
index 0c383a8baec..94eba319a17 100644
--- a/doc/user/project/import/manifest.md
+++ b/doc/user/project/import/manifest.md
@@ -51,13 +51,13 @@ As a result, the following projects are created:
## Import the repositories
-Follow these steps to start the import:
+To start the import:
-1. From your GitLab dashboard click **New project**
-1. Switch to the **Import project** tab
-1. Click on the **Manifest file** button
-1. Provide GitLab with a manifest XML file
-1. Select a group you want to import to (you need to create a group first if you don't have one)
+1. From your GitLab dashboard click **New project**.
+1. Switch to the **Import project** tab.
+1. Click on the **Manifest file** button.
+1. Provide GitLab with a manifest XML file.
+1. Select a group you want to import to (you need to create a group first if you don't have one).
1. Click **List available repositories**. At this point, you are redirected
to the import status page with projects list based on the manifest file.
1. Check the list and click **Import all repositories** to start the import.
diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md
index 2a58a52ce2c..e39976e00f6 100644
--- a/doc/user/project/import/svn.md
+++ b/doc/user/project/import/svn.md
@@ -16,7 +16,7 @@ There are two approaches to SVN to Git migration:
- [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
- Makes the GitLab repository to mirror the SVN project.
- Git and SVN repositories are kept in sync; you can use either one.
- - Smoothens the migration process and allows to manage migration risks.
+ - Smoothens the migration process and allows you to manage migration risks.
- [Cut over migration](#cut-over-migration-with-svn2git) which:
- Translates and imports the existing data and history from SVN to Git.
diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md
index ef44989d7f2..705df686fe0 100644
--- a/doc/user/project/import/tfvc.md
+++ b/doc/user/project/import/tfvc.md
@@ -46,12 +46,8 @@ Advantages of migrating to Git/GitLab:
Migration options from TFVC to Git depend on your operating system.
-- If you're migrating on Microsoft Windows:
-
- - Use the [`git-tfs`](https://github.com/git-tfs/git-tfs)
- tool. See [Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md)
- for details.
-
-- If you're on a Unix-based system:
-
- - Follow the procedures described with this [TFVC to Git migration tool](https://github.com/turbo/gtfotfs).
+- If you're migrating on Microsoft Windows, use the [`git-tfs`](https://github.com/git-tfs/git-tfs)
+ tool. See [Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md)
+ for details.
+- If you're on a Unix-based system, follow the procedures described with this
+ [TFVC to Git migration tool](https://github.com/turbo/gtfotfs).
diff --git a/doc/user/project/members/img/add_user_give_permissions_v13_8.png b/doc/user/project/members/img/add_user_give_permissions_v13_8.png
deleted file mode 100644
index 1916d056a52..00000000000
--- a/doc/user/project/members/img/add_user_give_permissions_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.png b/doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.png
deleted file mode 100644
index a6dddec3fb7..00000000000
--- a/doc/user/project/members/img/add_user_import_members_from_another_project_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_imported_members_v13_9.png b/doc/user/project/members/img/add_user_imported_members_v13_9.png
deleted file mode 100644
index e40240df2b2..00000000000
--- a/doc/user/project/members/img/add_user_imported_members_v13_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_list_members_v13_9.png b/doc/user/project/members/img/add_user_list_members_v13_9.png
deleted file mode 100644
index 7a07ea01d14..00000000000
--- a/doc/user/project/members/img/add_user_list_members_v13_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_search_people_v13_8.png b/doc/user/project/members/img/add_user_search_people_v13_8.png
deleted file mode 100644
index e9aa58512ab..00000000000
--- a/doc/user/project/members/img/add_user_search_people_v13_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 127cd4a698f..7dc1a9c612f 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -6,19 +6,75 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Members of a project
-You can manage the groups and users and their access levels in all of your
-projects. You can also personalize the access level you give each user,
-per-project.
+Members are the users and groups who have access to your project.
-You should have Maintainer or Owner [permissions](../../permissions.md) to add
-or import a new user to your project.
+Each member gets a role, which determines what they can do in the project.
-To view, edit, add, and remove project's members, go to your
-project's **Members**.
+## Add users to a project
+
+Add users to a project so they become members and have permission
+to perform actions.
+
+Prerequisite:
+
+- You must have maintainer or owner [permissions](../../permissions.md).
+
+To add a user to a project:
+
+1. Go to your project and select **Members**.
+1. On the **Invite member** tab, under **GitLab member of Email address**, type the username or email address.
+1. Select a [role](../../permissions.md).
+1. Optional. Choose an expiration date. On that date, the user can no longer access the project.
+1. Select **Invite**.
+
+If the user has a GitLab account, they are added to the members list.
+If you used an email address, the user receives an email.
+
+## Add groups to a project
+
+When you assign a group to a project, each user in the group gets access to the project,
+based on the role they're assigned in the group. However, the user's access is also
+limited by the maximum role you choose when you invite the group.
+
+Prerequisite:
+
+- You must have maintainer or owner [permissions](../../permissions.md).
+
+To add groups to a project:
+
+1. Go to your project and select **Members**.
+1. On the **Invite group** tab, under **Select a group to invite**, choose a group.
+1. Select the highest max [role](../../permissions.md) for users in the group.
+1. Optional. Choose an expiration date. On that date, the user can no longer access the project.
+1. Select **Invite**.
+
+The members of the group are not displayed on the **Members** tab.
+The **Members** tab shows:
+
+- Members who are directly assigned to the project.
+- If the project was created in a group [namespace](../../group/index.md#namespaces), members of that group.
+
+## Import users from another project
+
+You can import another project's users to your own project. Users
+retain the same permissions as the project you import them from.
+
+Prerequisite:
+
+- You must have maintainer or owner [permissions](../../permissions.md).
+
+To import users:
+
+1. Go to your project and select **Members**.
+1. On the **Invite member** tab, at the bottom of the panel, select **Import**.
+1. Select the project. You can view only the projects for which you're a maintainer.
+1. Select **Import project members**.
+
+A success message is displayed and the new members are now displayed in the list.
## Inherited membership
-When your project belongs to the group, group members inherit the membership and permission
+When your project belongs to a group, group members inherit the membership and permission
level for the project from the group.
![Project members page](img/project_members_v13_9.png)
@@ -70,48 +126,6 @@ You can sort members by **Account**, **Access granted**, **Max role**, or **Last
![Project members sort](img/project_members_sort_v13_9.png)
-## Add a user
-
-Right next to **People**, start typing the name or username of the user you
-want to add.
-
-![Search for people](img/add_user_search_people_v13_8.png)
-
-Select the user and the [permission level](../../permissions.md)
-that you'd like to give the user. You can add more than one user at a time.
-The Owner role can only be assigned at the group level.
-
-![Give user permissions](img/add_user_give_permissions_v13_8.png)
-
-Once done, select **Add users to project** and they are immediately added to
-your project with the permissions you gave them above.
-
-![List members](img/add_user_list_members_v13_9.png)
-
-From there on, you can either remove an existing user or change their access
-level to the project.
-
-## Import users from another project
-
-You can import another project's users to your own project. Users
-retain the same permissions as the project you import them from.
-
-To import users:
-
-1. Go to your project and select **Members**.
-
-1. On the **Invite member** tab, select **Import**.
-
-1. Select the project. You can only view projects you are Maintainer of.
-
- ![Import members from another project](img/add_user_import_members_from_another_project_v13_8.png)
-
-1. Select **Import project members**. A message displays, notifying you
- that the import was successful, and the new members are now in the project's
- members list.
-
-![Members list of new members](img/add_user_imported_members_v13_9.png)
-
## Invite people using their e-mail address
NOTE:
@@ -240,8 +254,7 @@ requests they are currently assigned or leave the assignments as they are.
To remove a member from a project:
-1. In a project, go to **{users}** **Members**.
-1. Click the **Delete** **{remove}** button next to a project member you want to remove.
- A **Remove member** modal appears.
-1. (Optional) Select the **Also unassign this user from related issues and merge requests** checkbox.
-1. Click **Remove member**.
+1. Go to your project and select **Members**.
+1. Next to the project member you want to remove, select **Remove member** **{remove}**.
+1. Optional. In the confirmation box, select the **Also unassign this user from related issues and merge requests** checkbox.
+1. Select **Remove member**.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 5e5643c7109..085e4db0b94 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -4,7 +4,7 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Share Projects with other Groups
+# Share projects with other groups
You can share projects with other [groups](../../group/index.md). This makes it
possible to add a group of users to a project with a single action.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index c66f9038ed2..673a756f18d 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -7,46 +7,45 @@ type: reference, howto
# Protected branches **(FREE)**
-[Permissions](../permissions.md) in GitLab are fundamentally defined around the
+In GitLab, [permissions](../permissions.md) are fundamentally defined around the
idea of having read or write permission to the repository and branches. To impose
further restrictions on certain branches, they can be protected.
-## Overview
+The default branch for your repository is protected by default.
-By default, a protected branch does these things:
+## Who can access a protected branch
-- It prevents its creation, if not already created, from everybody except users
- with Maintainer permission.
-- It prevents pushes from everybody except users with **Allowed** permission.
-- It prevents **anyone** from force pushing to the branch.
-- It prevents **anyone** from deleting the branch.
+When a branch is protected, the default behavior enforces
+these restrictions on the branch.
-**Permissions:**
+| Action | Who can do it |
+|--------------------------|---------------|
+| Protect a branch | Maintainers only. |
+| Push to the branch | GitLab administrators and anyone with **Allowed** permission. (*) |
+| Force push to the branch | No one. |
+| Delete the branch | No one. |
-- GitLab administrators are allowed to push to the protected branches.
-- Users with [Developer permissions](../permissions.md) are allowed to
- create a project in a group, but might not be allowed to initially
- push to the [default branch](repository/branches/default.md).
+(*) Users with developer permissions can create a project in a group,
+but might not be allowed to initially push to the [default branch](repository/branches/default.md).
-The default branch protection level is set in the [Admin Area](../admin_area/settings/visibility_and_access_controls.md#default-branch-protection).
+### Set the branch protection default level
-See the [Changelog](#changelog) section for changes over time.
+The default branch protection level is set in the [Admin Area](../admin_area/settings/visibility_and_access_controls.md#default-branch-protection).
-## Configuring protected branches
+## Configure a protected branch
-To protect a branch, you need to have at least Maintainer permission level.
-The default branch for your repository is protected by default.
+Prerequisite:
-1. In your project, go to **Settings > Repository**.
-1. Scroll to find the **Protected branches** section.
-1. From the **Branch** dropdown menu, select the branch you want to protect and
- click **Protect**. In the screenshot below, we chose the `develop` branch.
+- You must have at least maintainer permissions.
- ![Protected branches page](img/protected_branches_page_v12_3.png)
+To protect a branch:
-1. Once done, the protected branch displays in the **Protected branches** list.
+1. Go to your project and select **Settings > Repository**.
+1. Expand **Protected branches**.
+1. From the **Branch** dropdown menu, select the branch you want to protect.
+1. Select **Protect**.
- ![Protected branches list](img/protected_branches_list_v12_3.png)
+The protected branch displays in the **Protected branches** list.
## Using the Allowed to merge and Allowed to push settings
@@ -141,7 +140,7 @@ all matching branches:
![Protected branch matches](img/protected_branches_matches.png)
-## Creating a protected branch
+## Create a protected branch
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53361) in GitLab 11.9.
@@ -161,7 +160,7 @@ To create a new branch through the user interface:
base the new branch on. Only existing protected branches and commits
that are already in protected branches are accepted.
-## Deleting a protected branch
+## Delete a protected branch
From time to time, you may need to delete or clean up protected branches.
User with [Maintainer permissions](../permissions.md) and greater can manually delete protected
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index db361869ed7..dddd7971f1b 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -774,9 +774,9 @@ module API
def self.service_classes
[
- ::AsanaService,
- ::AssemblaService,
- ::BambooService,
+ ::Integrations::Asana,
+ ::Integrations::Assembla,
+ ::Integrations::Bamboo,
::BugzillaService,
::BuildkiteService,
::ConfluenceService,
diff --git a/lib/gitlab/integrations/sti_type.rb b/lib/gitlab/integrations/sti_type.rb
new file mode 100644
index 00000000000..9ad28bba520
--- /dev/null
+++ b/lib/gitlab/integrations/sti_type.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Integrations
+ class StiType < ActiveRecord::Type::String
+ NAMESPACED_INTEGRATIONS = Set.new(%w(
+ Asana Assembla Bamboo
+ )).freeze
+
+ def cast(value)
+ new_cast(value) || super
+ end
+
+ def serialize(value)
+ new_serialize(value) || super
+ end
+
+ def deserialize(value)
+ value
+ end
+
+ def changed?(original_value, value, _new_value_before_type_cast)
+ original_value != serialize(value)
+ end
+
+ def changed_in_place?(original_value_for_database, value)
+ original_value_for_database != serialize(value)
+ end
+
+ private
+
+ def new_cast(value)
+ value = prepare_value(value)
+ return unless value
+
+ stripped_name = value.delete_suffix('Service')
+ return unless NAMESPACED_INTEGRATIONS.include?(stripped_name)
+
+ "Integrations::#{stripped_name}"
+ end
+
+ def new_serialize(value)
+ value = prepare_value(value)
+ return unless value&.starts_with?('Integrations::')
+
+ "#{value.delete_prefix('Integrations::')}Service"
+ end
+
+ # Returns value cast to a `String`, or `nil` if value is `nil`.
+ def prepare_value(value)
+ return value if value.nil? || value.is_a?(String)
+
+ value.to_s
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric.rb
new file mode 100644
index 00000000000..9c92f2e9595
--- /dev/null
+++ b/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountUsersUsingApproveQuickActionMetric < RedisHLLMetric
+ event_names :i_quickactions_approve
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric.rb b/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric.rb
index ed0ddb1cbbe..140d56f0d42 100644
--- a/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric.rb
@@ -12,7 +12,7 @@ module Gitlab
# end
class << self
def event_names(events = nil)
- @mentric_events = events
+ @metric_events = events
end
attr_reader :metric_events
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 57dfcc0ea36..1c314d85098 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -427,12 +427,14 @@ module Gitlab
def services_usage
# rubocop: disable UsageData/LargeTable:
Service.available_services_names.each_with_object({}) do |service_name, response|
- response["projects_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where(type: "#{service_name}_service".camelize))
- response["groups_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where(type: "#{service_name}_service".camelize))
- response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: "#{service_name}_service".camelize))
- response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: "#{service_name}_service".camelize))
- response["projects_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
- response["groups_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
+ service_type = Service.service_name_to_type(service_name)
+
+ response["projects_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where(type: service_type))
+ response["groups_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where(type: service_type))
+ response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: service_type))
+ response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: service_type))
+ response["projects_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: service_type))
+ response["groups_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: service_type))
end.merge(jira_usage, jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end
diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb
index 014d1f517bb..d81e413f4a9 100644
--- a/lib/sidebars/menu.rb
+++ b/lib/sidebars/menu.rb
@@ -13,7 +13,7 @@ module Sidebars
include ::Sidebars::Concerns::ContainerWithHtmlOptions
include ::Sidebars::Concerns::HasActiveRoutes
- attr_reader :context, :items
+ attr_reader :context
delegate :current_user, :container, to: :@context
def initialize(context)
@@ -29,7 +29,7 @@ module Sidebars
override :render?
def render?
- has_items?
+ has_renderable_items?
end
# Menus might have or not a link
@@ -43,7 +43,7 @@ module Sidebars
# This method filters the information and returns: { path: ['foo', 'bar'], controller: :foo }
def all_active_routes
@all_active_routes ||= begin
- ([active_routes] + items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
+ ([active_routes] + renderable_items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
pairs.each do |k, v|
hash[k] ||= []
hash[k] += Array(v)
@@ -55,10 +55,22 @@ module Sidebars
end
end
+ # Returns whether the menu has any menu item, no
+ # matter whether it is renderable or not
def has_items?
@items.any?
end
+ # Returns all renderable menu items
+ def renderable_items
+ @renderable_items ||= @items.select(&:render?)
+ end
+
+ # Returns whether the menu has any renderable menu item
+ def has_renderable_items?
+ renderable_items.any?
+ end
+
def add_item(item)
add_element(@items, item)
end
diff --git a/lib/sidebars/menu_item.rb b/lib/sidebars/menu_item.rb
index 917ddb1fd13..b0a12e769dc 100644
--- a/lib/sidebars/menu_item.rb
+++ b/lib/sidebars/menu_item.rb
@@ -18,5 +18,9 @@ module Sidebars
def show_hint?
hint_html_options.present?
end
+
+ def render?
+ true
+ end
end
end
diff --git a/lib/sidebars/nil_menu_item.rb b/lib/sidebars/nil_menu_item.rb
new file mode 100644
index 00000000000..9ff7fd0d6d6
--- /dev/null
+++ b/lib/sidebars/nil_menu_item.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Sidebars
+ class NilMenuItem < MenuItem
+ extend ::Gitlab::Utils::Override
+
+ def initialize(item_id:)
+ super(item_id: item_id, title: nil, link: nil, active_routes: {})
+ end
+
+ override :render?
+ def render?
+ false
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/analytics_menu.rb b/lib/sidebars/projects/menus/analytics_menu.rb
index b8a4e0171a7..ef10a389572 100644
--- a/lib/sidebars/projects/menus/analytics_menu.rb
+++ b/lib/sidebars/projects/menus/analytics_menu.rb
@@ -21,7 +21,7 @@ module Sidebars
def link
return cycle_analytics_menu_item.link if cycle_analytics_menu_item
- items.first.link
+ renderable_items.first.link
end
override :extra_container_html_options
diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb
index 51a1df7f1d3..ed790db85ed 100644
--- a/lib/sidebars/projects/menus/packages_registries_menu.rb
+++ b/lib/sidebars/projects/menus/packages_registries_menu.rb
@@ -15,7 +15,7 @@ module Sidebars
override :link
def link
- items.first.link
+ renderable_items.first.link
end
override :title
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b54714254be..c479217861d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1895,6 +1895,9 @@ msgstr ""
msgid "Add a bullet list"
msgstr ""
+msgid "Add a collapsible section"
+msgstr ""
+
msgid "Add a comment to this line"
msgstr ""
@@ -5556,9 +5559,6 @@ msgstr ""
msgid "By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. "
msgstr ""
-msgid "By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}"
-msgstr ""
-
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
msgstr ""
@@ -14532,6 +14532,9 @@ msgstr ""
msgid "Geo|%{component} synced"
msgstr ""
+msgid "Geo|%{component} verified"
+msgstr ""
+
msgid "Geo|%{eventId} (%{timeAgo})"
msgstr ""
@@ -29837,6 +29840,12 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
+msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}"
+msgstr ""
+
+msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}"
+msgstr ""
+
msgid "SignUp|First name is too long (maximum is %{max_length} characters)."
msgstr ""
diff --git a/package.json b/package.json
index 81a221643f0..5463bd0c63c 100644
--- a/package.json
+++ b/package.json
@@ -199,7 +199,7 @@
"commander": "^2.18.0",
"custom-jquery-matchers": "^2.1.0",
"docdash": "^1.0.2",
- "eslint": "7.25.0",
+ "eslint": "7.26.0",
"eslint-import-resolver-jest": "3.0.0",
"eslint-import-resolver-webpack": "0.13.0",
"eslint-plugin-jasmine": "4.1.2",
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index c839c35450e..c5e6479ec51 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -26,6 +26,18 @@ RSpec.describe 'Groups > Members > Manage members' do
end
end
+ shared_examples 'does not include either invite modal or either invite form' do
+ it 'does not include either of the invite members or invite group modal buttons' do
+ expect(page).not_to have_selector '.js-invite-members-modal'
+ expect(page).not_to have_selector '.js-invite-group-modal'
+ end
+
+ it 'does not include either of the invite users or invite group forms' do
+ expect(page).not_to have_selector '.invite-users-form'
+ expect(page).not_to have_selector '.invite-group-form'
+ end
+ end
+
context 'when Invite Members modal is enabled' do
it_behaves_like 'includes the correct Invite link', '.js-invite-members-trigger', '.invite-users-form'
it_behaves_like 'includes the correct Invite link', '.js-invite-group-trigger', '.invite-group-form'
@@ -165,23 +177,46 @@ RSpec.describe 'Groups > Members > Manage members' do
end
end
- it 'guest can not manage other users', :js do
- group.add_guest(user1)
- group.add_developer(user2)
+ context 'as a guest', :js do
+ before do
+ group.add_guest(user1)
+ group.add_developer(user2)
- visit group_group_members_path(group)
+ visit group_group_members_path(group)
+ end
- expect(page).not_to have_selector '.invite-users-form'
- expect(page).not_to have_selector '.invite-group-form'
- expect(page).not_to have_selector '.js-invite-members-modal'
- expect(page).not_to have_selector '.js-invite-group-modal'
+ it_behaves_like 'does not include either invite modal or either invite form'
- page.within(second_row) do
- # Can not modify user2 role
- expect(page).not_to have_button 'Developer'
+ it 'does not include a button on the members page list to manage or remove the existing member', :js do
+ page.within(second_row) do
+ # Can not modify user2 role
+ expect(page).not_to have_button 'Developer'
+
+ # Can not remove user2
+ expect(page).not_to have_selector 'button[title="Remove member"]'
+ end
+ end
+ end
+
+ context 'As a guest when the :invite_members_group_modal feature flag is disabled', :js do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ group.add_guest(user1)
+ group.add_developer(user2)
+
+ visit group_group_members_path(group)
+ end
+
+ it_behaves_like 'does not include either invite modal or either invite form'
+
+ it 'does not include a button on the members page list to manage or remove the existing member', :js do
+ page.within(second_row) do
+ # Can not modify user2 role
+ expect(page).not_to have_button 'Developer'
- # Can not remove user2
- expect(page).not_to have_selector 'button[title="Remove member"]'
+ # Can not remove user2
+ expect(page).not_to have_selector 'button[title="Remove member"]'
+ end
end
end
end
diff --git a/spec/features/projects/services/user_activates_asana_spec.rb b/spec/features/projects/integrations/user_activates_asana_spec.rb
index cf2290383e8..cf2290383e8 100644
--- a/spec/features/projects/services/user_activates_asana_spec.rb
+++ b/spec/features/projects/integrations/user_activates_asana_spec.rb
diff --git a/spec/features/projects/services/user_activates_assembla_spec.rb b/spec/features/projects/integrations/user_activates_assembla_spec.rb
index 63cc424a641..63cc424a641 100644
--- a/spec/features/projects/services/user_activates_assembla_spec.rb
+++ b/spec/features/projects/integrations/user_activates_assembla_spec.rb
diff --git a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
index 91db375be3a..91db375be3a 100644
--- a/spec/features/projects/services/user_activates_atlassian_bamboo_ci_spec.rb
+++ b/spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 9a2068a27a1..0da2f84f2a1 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -2,6 +2,7 @@ import { GlBanner, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+import eventHub from '~/invite_members/event_hub';
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils');
@@ -58,12 +59,23 @@ describe('InviteMembersBanner', () => {
});
});
- it('sets the button attributes for the buttonClickEvent', () => {
- const button = wrapper.find(`[href='${wrapper.vm.inviteMembersPath}']`);
+ describe('when the button is clicked', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ wrapper.find(GlBanner).vm.$emit('primary');
+ });
+
+ it('calls openModal through the eventHub', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('openModal', {
+ inviteeType: 'members',
+ source: 'invite_members_banner',
+ });
+ });
- expect(button.attributes()).toMatchObject({
- 'data-track-event': buttonClickEvent,
- 'data-track-label': trackLabel,
+ it('sends the buttonClickEvent with correct trackCategory and trackLabel', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(trackCategory, buttonClickEvent, {
+ label: trackLabel,
+ });
});
});
@@ -100,10 +112,6 @@ describe('InviteMembersBanner', () => {
it('uses the button_text text from options for buttontext', () => {
expect(findBanner().attributes('buttontext')).toBe(buttonText);
});
-
- it('uses the href from inviteMembersPath for buttonlink', () => {
- expect(findBanner().attributes('buttonlink')).toBe(inviteMembersPath);
- });
});
describe('dismissing', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 077c2174571..fec6abc9639 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -48,6 +48,7 @@ describe('Markdown field header component', () => {
'Add a bullet list',
'Add a numbered list',
'Add a task list',
+ 'Add a collapsible section',
'Add a table',
'Go full screen',
];
@@ -133,6 +134,14 @@ describe('Markdown field header component', () => {
);
});
+ it('renders collapsible section template', () => {
+ const detailsBlockButton = findToolbarButtonByProp('icon', 'details-block');
+
+ expect(detailsBlockButton.props('tag')).toEqual(
+ '<details><summary>Click to expand</summary>\n{text}\n</details>',
+ );
+ });
+
it('does not render suggestion button if `canSuggest` is set to false', () => {
createWrapper({
canSuggest: false,
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 3edab40ac3f..122f2339b28 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -80,49 +80,6 @@ RSpec.describe InviteMembersHelper do
context 'with group' do
let_it_be(:group) { create(:group) }
- describe "#can_invite_members_for_group?" do
- include Devise::Test::ControllerHelpers
-
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- allow(helper).to receive(:current_user) { user }
- end
-
- context 'when the user can admin_group_member' do
- before do
- allow(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.can_invite_members_for_group?(group)).to eq true
- expect(helper).to have_received(:can?).with(user, :admin_group_member, group)
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(invite_members_group_modal: false)
- end
-
- it 'returns false' do
- expect(helper.can_invite_members_for_group?(group)).to eq false
- expect(helper).not_to have_received(:can?)
- end
- end
- end
-
- context 'when the user can not admin_group_member' do
- before do
- expect(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
- end
-
- it 'returns false' do
- expect(helper.can_invite_members_for_group?(group)).to eq false
- end
- end
- end
-
describe "#invite_group_members?" do
context 'when the user is an owner' do
before do
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 8c08b06d8a8..a8a918cbc74 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -265,4 +265,32 @@ RSpec.describe NamespacesHelper do
end
end
end
+
+ describe '#cascading_namespace_setting_locked?' do
+ let(:attribute) { :delayed_project_removal }
+
+ context 'when `group` argument is `nil`' do
+ it 'returns `false`' do
+ expect(helper.cascading_namespace_setting_locked?(attribute, nil)).to eq(false)
+ end
+ end
+
+ context 'when `*_locked?` method does not exist' do
+ it 'returns `false`' do
+ expect(helper.cascading_namespace_setting_locked?(:attribute_that_does_not_exist, admin_group)).to eq(false)
+ end
+ end
+
+ context 'when `*_locked?` method does exist' do
+ before do
+ allow(admin_group.namespace_settings).to receive(:delayed_project_removal_locked?).and_return(true)
+ end
+
+ it 'calls corresponding `*_locked?` method' do
+ helper.cascading_namespace_setting_locked?(attribute, admin_group, include_self: true)
+
+ expect(admin_group.namespace_settings).to have_received(:delayed_project_removal_locked?).with(include_self: true)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/integrations/sti_type_spec.rb b/spec/lib/gitlab/integrations/sti_type_spec.rb
new file mode 100644
index 00000000000..7a023d6041c
--- /dev/null
+++ b/spec/lib/gitlab/integrations/sti_type_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Integrations::StiType do
+ let(:types) { ['AsanaService', 'Integrations::Asana', Integrations::Asana] }
+
+ describe '#serialize' do
+ context 'SQL SELECT' do
+ let(:expected_sql) do
+ <<~SQL.strip
+ SELECT "services".* FROM "services" WHERE "services"."type" = 'AsanaService'
+ SQL
+ end
+
+ it 'forms SQL SELECT statements correctly' do
+ sql_statements = types.map do |type|
+ Service.where(type: type).to_sql
+ end
+
+ expect(sql_statements).to all(eq(expected_sql))
+ end
+ end
+
+ context 'SQL CREATE' do
+ let(:expected_sql) do
+ <<~SQL.strip
+ INSERT INTO "services" ("type") VALUES ('AsanaService')
+ SQL
+ end
+
+ it 'forms SQL CREATE statements correctly' do
+ sql_statements = types.map do |type|
+ record = ActiveRecord::QueryRecorder.new { Service.insert({ type: type }) }
+ record.log.first
+ end
+
+ expect(sql_statements).to all(include(expected_sql))
+ end
+ end
+
+ context 'SQL UPDATE' do
+ let(:expected_sql) do
+ <<~SQL.strip
+ UPDATE "services" SET "type" = 'AsanaService'
+ SQL
+ end
+
+ let_it_be(:service) { create(:service) }
+
+ it 'forms SQL UPDATE statements correctly' do
+ sql_statements = types.map do |type|
+ record = ActiveRecord::QueryRecorder.new { service.update_column(:type, type) }
+ record.log.first
+ end
+
+ expect(sql_statements).to all(include(expected_sql))
+ end
+ end
+
+ context 'SQL DELETE' do
+ let(:expected_sql) do
+ <<~SQL.strip
+ DELETE FROM "services" WHERE "services"."type" = 'AsanaService'
+ SQL
+ end
+
+ let(:service) { create(:service) }
+
+ it 'forms SQL DELETE statements correctly' do
+ sql_statements = types.map do |type|
+ record = ActiveRecord::QueryRecorder.new { Service.delete_by(type: type) }
+ record.log.first
+ end
+
+ expect(sql_statements).to all(match(expected_sql))
+ end
+ end
+ end
+
+ describe '#deserialize' do
+ specify 'it deserializes type correctly', :aggregate_failures do
+ types.each do |type|
+ service = create(:service, type: type)
+
+ expect(service.type).to eq('AsanaService')
+ end
+ end
+ end
+
+ describe '#cast' do
+ it 'casts type as model correctly', :aggregate_failures do
+ create(:service, type: 'AsanaService')
+
+ types.each do |type|
+ expect(Service.find_by(type: type)).to be_kind_of(Integrations::Asana)
+ end
+ end
+ end
+
+ describe '#changed?' do
+ it 'detects changes correctly', :aggregate_failures do
+ service = create(:service, type: 'AsanaService')
+
+ types.each do |type|
+ service.type = type
+
+ expect(service).not_to be_changed
+ end
+
+ service.type = 'NewType'
+
+ expect(service).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb
new file mode 100644
index 00000000000..7adba825a13
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric, :clean_gitlab_redis_shared_state do
+ before do
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 1.week.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 2.weeks.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.weeks.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.months.ago)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'redis_hll' }, 2
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '7d', data_source: 'redis_hll' }, 1
+end
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 684f0bf17a1..2692e70cabf 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -25,6 +25,11 @@ RSpec.describe Gitlab::UsageDataMetrics do
it 'includes counts keys' do
expect(subject[:counts]).to include(:boards)
end
+
+ it 'includes i_quickactions_approve monthly and weekly key' do
+ expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_monthly)
+ expect(subject[:redis_hll_counters][:quickactions]).to include(:i_quickactions_approve_weekly)
+ end
end
end
end
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index 53483f0c924..7dcf1940442 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -5,15 +5,18 @@ require 'spec_helper'
RSpec.describe Sidebars::Menu do
let(:menu) { described_class.new(context) }
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+ let(:nil_menu_item) { Sidebars::NilMenuItem.new(item_id: :foo) }
describe '#all_active_routes' do
it 'gathers all active routes of items and the current menu' do
menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w(bar test) }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' }))
menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' }))
+ menu.add_item(nil_menu_item)
allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
+ expect(menu).to receive(:renderable_items).and_call_original
expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) })
end
end
@@ -31,6 +34,60 @@ RSpec.describe Sidebars::Menu do
expect(menu.render?).to be true
end
+
+ context 'when menu items are NilMenuItem' do
+ it 'returns false' do
+ menu.add_item(nil_menu_item)
+
+ expect(menu.render?).to be false
+ end
+ end
+ end
+ end
+
+ describe '#has_items?' do
+ it 'returns true when there are regular menu items' do
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}))
+
+ expect(menu.has_items?).to be true
+ end
+
+ it 'returns true when there are nil menu items' do
+ menu.add_item(nil_menu_item)
+
+ expect(menu.has_items?).to be true
+ end
+ end
+
+ describe '#has_renderable_items?' do
+ it 'returns true when there are regular menu items' do
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}))
+
+ expect(menu.has_renderable_items?).to be true
+ end
+
+ it 'returns false when there are nil menu items' do
+ menu.add_item(nil_menu_item)
+
+ expect(menu.has_renderable_items?).to be false
+ end
+
+ it 'returns true when there are both regular and nil menu items' do
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}))
+ menu.add_item(nil_menu_item)
+
+ expect(menu.has_renderable_items?).to be true
+ end
+ end
+
+ describe '#renderable_items' do
+ it 'returns only regular menu items' do
+ item = Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {})
+ menu.add_item(item)
+ menu.add_item(nil_menu_item)
+
+ expect(menu.renderable_items.size).to eq 1
+ expect(menu.renderable_items.first).to eq item
end
end
diff --git a/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb b/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
index c109631fabe..87ef9e162e2 100644
--- a/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/analytics_menu_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Sidebars::Projects::Menus::AnalyticsMenu do
context 'when menu does not have any menu items' do
it 'returns false' do
- allow(subject).to receive(:has_items?).and_return(false)
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
expect(subject.render?).to be false
end
@@ -49,13 +49,13 @@ RSpec.describe Sidebars::Projects::Menus::AnalyticsMenu do
it 'returns link to the the first visible menu item' do
allow(subject).to receive(:cycle_analytics_menu_item).and_return(nil)
- expect(subject.link).to eq subject.items.first.link
+ expect(subject.link).to eq subject.renderable_items.first.link
end
end
end
describe 'Menu items' do
- subject { described_class.new(context).items.index { |e| e.item_id == item_id } }
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
describe 'CI/CD' do
let(:item_id) { :ci_cd_analytics }
diff --git a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
index 89b03e1c918..dee2716e4c2 100644
--- a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
@@ -26,40 +26,44 @@ RSpec.describe Sidebars::Projects::Menus::CiCdMenu do
end
end
- describe 'Pipelines Editor' do
- subject { described_class.new(context).items.index { |e| e.item_id == :pipelines_editor } }
+ describe 'Menu items' do
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
- context 'when user cannot view pipeline editor' do
- let(:can_view_pipeline_editor) { false }
+ describe 'Pipelines Editor' do
+ let(:item_id) { :pipelines_editor }
- it 'does not include pipeline editor menu item' do
- is_expected.to be_nil
+ context 'when user cannot view pipeline editor' do
+ let(:can_view_pipeline_editor) { false }
+
+ it 'does not include pipeline editor menu item' do
+ is_expected.to be_nil
+ end
end
- end
- context 'when user can view pipeline editor' do
- it 'includes pipeline editor menu item' do
- is_expected.not_to be_nil
+ context 'when user can view pipeline editor' do
+ it 'includes pipeline editor menu item' do
+ is_expected.not_to be_nil
+ end
end
end
- end
- describe 'Artifacts' do
- subject { described_class.new(context).items.index { |e| e.item_id == :artifacts } }
+ describe 'Artifacts' do
+ let(:item_id) { :artifacts }
- context 'when feature flag :artifacts_management_page is disabled' do
- it 'does not include artifacts menu item' do
- stub_feature_flags(artifacts_management_page: false)
+ context 'when feature flag :artifacts_management_page is disabled' do
+ it 'does not include artifacts menu item' do
+ stub_feature_flags(artifacts_management_page: false)
- is_expected.to be_nil
+ is_expected.to be_nil
+ end
end
- end
- context 'when feature flag :artifacts_management_page is enabled' do
- it 'includes artifacts menu item' do
- stub_feature_flags(artifacts_management_page: true)
+ context 'when feature flag :artifacts_management_page is enabled' do
+ it 'includes artifacts menu item' do
+ stub_feature_flags(artifacts_management_page: true)
- is_expected.not_to be_nil
+ is_expected.not_to be_nil
+ end
end
end
end
diff --git a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
index c50696f0883..0ecb328efd1 100644
--- a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Sidebars::Projects::Menus::ConfluenceMenu do
end
it 'does not contain any sub menu' do
- expect(subject.items).to be_empty
+ expect(subject.has_items?).to be false
end
end
end
diff --git a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
index 65d2a866393..5d62eebca1c 100644
--- a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Sidebars::Projects::Menus::ExternalIssueTrackerMenu do
subject { described_class.new(context) }
it 'does not contain any sub menu' do
- expect(subject.items).to be_empty
+ expect(subject.has_items?).to be false
end
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
index b12f31017be..19efd2bbd6b 100644
--- a/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Sidebars::Projects::Menus::ExternalWikiMenu do
subject { described_class.new(context) }
it 'does not contain any sub menu' do
- expect(subject.items).to be_empty
+ expect(subject.has_items?).to be false
end
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
index f7cb0adbb66..44013898721 100644
--- a/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/hidden_menu_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Sidebars::Projects::Menus::HiddenMenu do
context 'when menu does not have any menu items' do
it 'returns false' do
- allow(subject).to receive(:has_items?).and_return(false)
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
expect(subject.render?).to be false
end
@@ -27,7 +27,7 @@ RSpec.describe Sidebars::Projects::Menus::HiddenMenu do
end
describe 'Menu items' do
- subject { described_class.new(context).items.index { |e| e.item_id == item_id } }
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
shared_examples 'access rights checks' do
specify { is_expected.not_to be_nil }
diff --git a/spec/lib/sidebars/projects/menus/labels_menu_spec.rb b/spec/lib/sidebars/projects/menus/labels_menu_spec.rb
index 58d82293b13..588119746cf 100644
--- a/spec/lib/sidebars/projects/menus/labels_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/labels_menu_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Sidebars::Projects::Menus::LabelsMenu do
subject { described_class.new(context) }
it 'does not contain any sub menu' do
- expect(subject.items).to be_empty
+ expect(subject.has_items?).to eq false
end
describe '#render?' do
diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
index dc1aecc6546..ef5ae550551 100644
--- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
subject { described_class.new(context) }
it 'does not contain any sub menu' do
- expect(subject.instance_variable_get(:@items)).to be_empty
+ expect(subject.has_items?).to be false
end
describe '#nav_link_html_options' do
diff --git a/spec/lib/sidebars/projects/menus/operations_menu_spec.rb b/spec/lib/sidebars/projects/menus/operations_menu_spec.rb
index 6e764dbb83a..5f52ae0fd7a 100644
--- a/spec/lib/sidebars/projects/menus/operations_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/operations_menu_spec.rb
@@ -21,9 +21,9 @@ RSpec.describe Sidebars::Projects::Menus::OperationsMenu do
end
context 'when operation feature is enabled' do
- context 'when menu does not have any menu items' do
+ context 'when menu does not have any renderable menu items' do
it 'returns false' do
- allow(subject).to receive(:has_items?).and_return(false)
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
expect(subject.render?).to be false
end
@@ -54,7 +54,7 @@ RSpec.describe Sidebars::Projects::Menus::OperationsMenu do
end
context 'Menu items' do
- subject { described_class.new(context).items.index { |e| e.item_id == item_id } }
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
describe 'Metrics Dashboard' do
let(:item_id) { :metrics }
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index fc0d7710a5e..731dd5eca23 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
describe '#render?' do
context 'when menu does not have any menu item to show' do
it 'returns false' do
- allow(subject).to receive(:has_items?).and_return(false)
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
expect(subject.render?).to eq false
end
@@ -36,7 +36,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
context 'when Packages Registry is visible' do
it 'menu link points to Packages Registry page' do
- expect(subject.link).to eq described_class.new(context).items.find { |i| i.item_id == :packages_registry }.link
+ expect(subject.link).to eq described_class.new(context).renderable_items.find { |i| i.item_id == :packages_registry }.link
end
end
@@ -44,95 +44,99 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
let(:packages_enabled) { false }
it 'menu link points to Container Registry page' do
- expect(subject.link).to eq described_class.new(context).items.find { |i| i.item_id == :container_registry }.link
+ expect(subject.link).to eq described_class.new(context).renderable_items.find { |i| i.item_id == :container_registry }.link
end
context 'when Container Registry is not visible' do
let(:registry_enabled) { false }
it 'menu link points to Infrastructure Registry page' do
- expect(subject.link).to eq described_class.new(context).items.find { |i| i.item_id == :infrastructure_registry }.link
+ expect(subject.link).to eq described_class.new(context).renderable_items.find { |i| i.item_id == :infrastructure_registry }.link
end
end
end
end
- describe 'Packages Registry' do
- subject { described_class.new(context).items.find { |i| i.item_id == :packages_registry }}
+ describe 'Menu items' do
+ subject { described_class.new(context).renderable_items.find { |i| i.item_id == item_id } }
- context 'when user can read packages' do
- context 'when config package setting is disabled' do
- it 'the menu item is not added to list of menu items' do
- stub_config(packages: { enabled: false })
+ describe 'Packages Registry' do
+ let(:item_id) { :packages_registry }
- is_expected.to be_nil
+ context 'when user can read packages' do
+ context 'when config package setting is disabled' do
+ it 'the menu item is not added to list of menu items' do
+ stub_config(packages: { enabled: false })
+
+ is_expected.to be_nil
+ end
end
- end
- context 'when config package setting is enabled' do
- it 'the menu item is added to list of menu items' do
- stub_config(packages: { enabled: true })
+ context 'when config package setting is enabled' do
+ it 'the menu item is added to list of menu items' do
+ stub_config(packages: { enabled: true })
- is_expected.not_to be_nil
+ is_expected.not_to be_nil
+ end
end
end
- end
- context 'when user cannot read packages' do
- let(:user) { nil }
+ context 'when user cannot read packages' do
+ let(:user) { nil }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
end
end
- end
- describe 'Container Registry' do
- subject { described_class.new(context).items.find { |i| i.item_id == :container_registry }}
+ describe 'Container Registry' do
+ let(:item_id) { :container_registry }
- context 'when user can read container images' do
- context 'when config registry setting is disabled' do
- it 'the menu item is not added to list of menu items' do
- stub_container_registry_config(enabled: false)
+ context 'when user can read container images' do
+ context 'when config registry setting is disabled' do
+ it 'the menu item is not added to list of menu items' do
+ stub_container_registry_config(enabled: false)
- is_expected.to be_nil
+ is_expected.to be_nil
+ end
end
- end
- context 'when config registry setting is enabled' do
- it 'the menu item is added to list of menu items' do
- stub_container_registry_config(enabled: true)
+ context 'when config registry setting is enabled' do
+ it 'the menu item is added to list of menu items' do
+ stub_container_registry_config(enabled: true)
- is_expected.not_to be_nil
+ is_expected.not_to be_nil
+ end
end
end
- end
- context 'when user cannot read container images' do
- let(:user) { nil }
+ context 'when user cannot read container images' do
+ let(:user) { nil }
- it 'the menu item is not added to list of menu items' do
- is_expected.to be_nil
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
end
end
- end
- describe 'Infrastructure Registry' do
- subject { described_class.new(context).items.find { |i| i.item_id == :infrastructure_registry }}
+ describe 'Infrastructure Registry' do
+ let(:item_id) { :infrastructure_registry }
- context 'when feature flag :infrastructure_registry_page is enabled' do
- it 'the menu item is added to list of menu items' do
- stub_feature_flags(infrastructure_registry_page: true)
+ context 'when feature flag :infrastructure_registry_page is enabled' do
+ it 'the menu item is added to list of menu items' do
+ stub_feature_flags(infrastructure_registry_page: true)
- is_expected.not_to be_nil
+ is_expected.not_to be_nil
+ end
end
- end
- context 'when feature flag :infrastructure_registry_page is disabled' do
- it 'the menu item is not added to list of menu items' do
- stub_feature_flags(infrastructure_registry_page: false)
+ context 'when feature flag :infrastructure_registry_page is disabled' do
+ it 'the menu item is not added to list of menu items' do
+ stub_feature_flags(infrastructure_registry_page: false)
- is_expected.to be_nil
+ is_expected.to be_nil
+ end
end
end
end
diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
index ddf9e779219..ed162910bdd 100644
--- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
describe 'Releases' do
- subject { described_class.new(context).items.index { |e| e.item_id == :releases } }
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == :releases } }
context 'when project repository is empty' do
it 'does not include releases menu item' do
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 85de46d1583..b87d991e434 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -11,14 +11,14 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
describe '#render?' do
it 'returns false when menu does not have any menu items' do
- allow(subject).to receive(:has_items?).and_return(false)
+ allow(subject).to receive(:has_renderable_items?).and_return(false)
expect(subject.render?).to be false
end
end
describe 'Menu items' do
- subject { described_class.new(context).items.index { |e| e.item_id == item_id } }
+ subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
shared_examples 'access rights checks' do
specify { is_expected.not_to be_nil }
diff --git a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
index 21336d70ad2..41447ee24a9 100644
--- a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Sidebars::Projects::Menus::WikiMenu do
subject { described_class.new(context) }
it 'does not contain any sub menu' do
- expect(subject.items).to be_empty
+ expect(subject.has_items?).to be false
end
describe '#render?' do
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/integrations/asana_spec.rb
index 7a6fe4b1537..4473478910a 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/integrations/asana_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AsanaService do
+RSpec.describe Integrations::Asana do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -54,7 +54,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment).with(text: expected_message)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
@asana.execute(data)
end
@@ -64,7 +64,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
@asana.execute(data)
end
@@ -74,7 +74,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
@asana.execute(data)
end
@@ -88,25 +88,25 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
d2 = double('Asana::Resources::Task')
expect(d2).to receive(:add_comment)
expect(d2).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
d3 = double('Asana::Resources::Task')
expect(d3).to receive(:add_comment)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
d4 = double('Asana::Resources::Task')
expect(d4).to receive(:add_comment)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
d5 = double('Asana::Resources::Task')
expect(d5).to receive(:add_comment)
expect(d5).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
@asana.execute(data)
end
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/integrations/assembla_spec.rb
index 207add6f090..bf9033416e9 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/integrations/assembla_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AssemblaService do
+RSpec.describe Integrations::Assembla do
include StubRequests
describe "Associations" do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/integrations/bamboo_spec.rb
index 45afbcca96d..0ba1595bbd8 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 7f06c8a87eb..62f97873a06 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ChatNotificationService do
end
describe 'validations' do
- it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]) }
+ it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
end
describe '#can_test?' do
@@ -127,6 +127,22 @@ RSpec.describe ChatNotificationService do
end
end
+ context 'when labels_to_be_notified_behavior is blank' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter, labels_to_be_notified_behavior: '') }
+
+ context 'no matching labels' do
+ let(:label_filter) { '~some random label' }
+
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'only one label matches' do
+ let(:label_filter) { '~some random label, ~Bug' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+ end
+
context 'when labels_to_be_notified_behavior is match_any' do
subject(:chat_service) do
described_class.new(
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 6b18d1f0cfa..e471e50498d 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -248,7 +248,7 @@ RSpec.describe Service do
describe '.find_or_initialize_all_non_project_specific' do
shared_examples 'service instances' do
it 'returns the available service instances' do
- expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false))
+ expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).map(&:to_param)).to match_array(Service.available_services_names(include_project_specific: false))
end
it 'does not create service instances' do
@@ -666,9 +666,22 @@ RSpec.describe Service do
end
end
+ describe '.service_name_to_model' do
+ it 'returns the model for the given service name', :aggregate_failures do
+ expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana)
+ # TODO We can remove this test when all models have been namespaced:
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955
+ expect(described_class.service_name_to_model('youtrack')).to eq(YoutrackService)
+ end
+
+ it 'raises an error if service name is invalid' do
+ expect { described_class.service_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
+ end
+ end
+
describe "{property}_changed?" do
let(:service) do
- BambooService.create(
+ Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
@@ -708,7 +721,7 @@ RSpec.describe Service do
describe "{property}_touched?" do
let(:service) do
- BambooService.create(
+ Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
@@ -748,7 +761,7 @@ RSpec.describe Service do
describe "{property}_was" do
let(:service) do
- BambooService.create(
+ Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
diff --git a/spec/services/admin/propagate_service_template_spec.rb b/spec/services/admin/propagate_service_template_spec.rb
index d95d31ceaea..b3ca7601cd6 100644
--- a/spec/services/admin/propagate_service_template_spec.rb
+++ b/spec/services/admin/propagate_service_template_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Admin::PropagateServiceTemplate do
context 'with a project that has another service' do
before do
- BambooService.create!(
+ Integrations::Bamboo.create!(
active: true,
project: project,
properties: {
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 4e943a40034..13b6b9283c3 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -6,7 +6,7 @@ Service.available_services_names.each do |service|
let(:dashed_service) { service.dasherize }
let(:service_method) { "#{service}_service".to_sym }
- let(:service_klass) { "#{service}_service".classify.constantize }
+ let(:service_klass) { Service.service_name_to_model(service) }
let(:service_instance) { service_klass.new }
let(:service_fields) { service_instance.fields }
let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
diff --git a/spec/views/devise/shared/_signup_box.html.haml_spec.rb b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
new file mode 100644
index 00000000000..b73e32fa765
--- /dev/null
+++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'devise/shared/_signup_box' do
+ before do
+ stub_devise
+ allow(view).to receive(:show_omniauth_providers).and_return(false)
+ allow(view).to receive(:url).and_return('_url_')
+ allow(view).to receive(:terms_path).and_return('_terms_path_')
+ allow(view).to receive(:button_text).and_return('_button_text_')
+ allow(view).to receive(:suggestion_path).and_return('_suggestion_path_')
+ stub_template 'devise/shared/_error_messages.html.haml' => ''
+ end
+
+ context 'when terms are enforced' do
+ before do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enforce_terms?).and_return(true)
+ end
+
+ it 'shows expected text with placeholders' do
+ render
+
+ expect(rendered).to have_content('By clicking _button_text_')
+ expect(rendered).to have_link('Terms of Use and Privacy Policy')
+ end
+
+ context 'when on .com' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
+ end
+
+ it 'shows expected GitLab text' do
+ render
+
+ expect(rendered).to have_content('I have read and accepted the GitLab Terms')
+ end
+ end
+
+ context 'when not on .com' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(false)
+ end
+
+ it 'shows expected text without GitLab' do
+ render
+
+ expect(rendered).to have_content('I have read and accepted the Terms')
+ end
+ end
+ end
+
+ context 'when terms are not enforced' do
+ before do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enforce_terms?).and_return(false)
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
+ end
+
+ it 'shows expected text with placeholders' do
+ render
+
+ expect(rendered).not_to have_content('By clicking')
+ end
+ end
+
+ def stub_devise
+ allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
+ allow(view).to receive(:resource).and_return(spy)
+ allow(view).to receive(:resource_name).and_return(:user)
+ end
+end
diff --git a/spec/views/groups/show.html.haml_spec.rb b/spec/views/groups/show.html.haml_spec.rb
new file mode 100644
index 00000000000..f40b03fda2a
--- /dev/null
+++ b/spec/views/groups/show.html.haml_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/edit.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ describe '"Share with group lock" setting' do
+ let(:root_owner) { create(:user) }
+ let(:root_group) { create(:group) }
+
+ before do
+ root_group.add_owner(root_owner)
+ end
+
+ shared_examples_for '"Share with group lock" setting' do |checkbox_options|
+ it 'has the correct label, help text, and checkbox options' do
+ assign(:group, test_group)
+ allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
+ allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
+ allow(view).to receive(:current_user).and_return(test_user)
+ expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
+ expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
+
+ render
+
+ expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
+ expect(rendered).to have_css('.js-descr', text: 'help text here')
+ expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
+ end
+ end
+
+ context 'for a root group' do
+ let(:test_group) { root_group }
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'for a subgroup' do
+ let!(:subgroup) { create(:group, parent: root_group) }
+ let(:sub_owner) { create(:user) }
+ let(:test_group) { subgroup }
+
+ context 'when the root_group has "Share with group lock" disabled' do
+ context 'when the subgroup has "Share with group lock" disabled' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+ end
+ end
+
+ context 'when the root_group has "Share with group lock" enabled' do
+ before do
+ root_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index bb15c167213..0e09c594867 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -847,10 +847,10 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
-"@eslint/eslintrc@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547"
- integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==
+"@eslint/eslintrc@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14"
+ integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
@@ -2751,16 +2751,11 @@ bootstrap-vue@2.17.3:
portal-vue "^2.1.7"
vue-functional-data-merge "^3.1.0"
-bootstrap@4.5.3:
+bootstrap@4.5.3, "bootstrap@>=4.5.2 <5.0.0":
version "4.5.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==
-"bootstrap@>=4.5.2 <5.0.0":
- version "4.6.0"
- resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
- integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
-
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
@@ -4952,13 +4947,13 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
-eslint@7.25.0:
- version "7.25.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67"
- integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==
+eslint@7.26.0:
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.26.0.tgz#d416fdcdcb3236cd8f282065312813f8c13982f6"
+ integrity sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg==
dependencies:
"@babel/code-frame" "7.12.11"
- "@eslint/eslintrc" "^0.4.0"
+ "@eslint/eslintrc" "^0.4.1"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"