diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-03 00:10:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-03 00:10:20 +0300 |
commit | df52f8c8af971b336f520e291da02e1ba6a1a9dd (patch) | |
tree | 41bb94e2d74f8c43e83abaf04f7e4e35cfd56251 | |
parent | f3c61892ecbcad3bfe57f06f197ae9e8996970db (diff) |
Add latest changes from gitlab-org/gitlab@master
40 files changed, 215 insertions, 50 deletions
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 72f49123190..ddcd49dafb0 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -6,7 +6,7 @@ workflow: include: - project: gitlab-org/quality/pipeline-common - ref: 7.1.0 + ref: 7.2.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 1528d1d6716..e3a5b365cee 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -01c879aca2c628e691e28d791f24185a22db55f2 +1ed139ab2d95e27491b49fcced61d0dc99834ab2 diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue index c37251af0ab..427e6c14327 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue @@ -72,6 +72,10 @@ export default { update: s__('BroadcastMessages|Update broadcast message'), updateError: s__('BroadcastMessages|There was an error updating broadcast message.'), cancel: __('Cancel'), + showInCli: s__('BroadcastMessages|Git remote responses'), + showInCliDescription: s__( + 'BroadcastMessages|Show the broadcast message in a command-line interface as a Git remote response', + ), }, messageThemes: THEMES, messageTypes: TYPES, @@ -97,6 +101,7 @@ export default { startsAt: new Date(this.broadcastMessage.startsAt.getTime()), endsAt: new Date(this.broadcastMessage.endsAt.getTime()), renderedMessage: '', + showInCli: this.broadcastMessage.showInCli, }; }, computed: { @@ -127,6 +132,7 @@ export default { target_access_levels: this.targetAccessLevels, starts_at: this.startsAt.toISOString(), ends_at: this.endsAt.toISOString(), + show_in_cli: this.showInCli, }); }, }, @@ -226,6 +232,17 @@ export default { <span>{{ $options.i18n.dismissableDescription }}</span> </gl-form-checkbox> </gl-form-group> + + <gl-form-group :label="$options.i18n.showInCli" label-for="show-in-cli-checkbox"> + <gl-form-checkbox + id="show-in-cli-checkbox" + v-model="showInCli" + class="gl-mt-3" + data-testid="show-in-cli-checkbox" + > + <span>{{ $options.i18n.showInCliDescription }}</span> + </gl-form-checkbox> + </gl-form-group> </template> <gl-form-group :label="$options.i18n.targetRoles" data-testid="target-roles-checkboxes"> diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js index 9f64b2dcaa0..ed137181a48 100644 --- a/app/assets/javascripts/admin/broadcast_messages/constants.js +++ b/app/assets/javascripts/admin/broadcast_messages/constants.js @@ -30,4 +30,5 @@ export const NEW_BROADCAST_MESSAGE = { targetAccessLevels: [], startsAt: new Date(), endsAt: new Date(), + showInCli: true, }; diff --git a/app/assets/javascripts/admin/broadcast_messages/edit.js b/app/assets/javascripts/admin/broadcast_messages/edit.js index 91dae949d45..33b3b028c58 100644 --- a/app/assets/javascripts/admin/broadcast_messages/edit.js +++ b/app/assets/javascripts/admin/broadcast_messages/edit.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import MessageForm from './components/message_form.vue'; export default () => { @@ -16,6 +17,7 @@ export default () => { targetPath, startsAt, endsAt, + showInCli, } = el.dataset; return new Vue({ @@ -34,11 +36,12 @@ export default () => { message, broadcastType, theme, - dismissable: dismissable === 'true', + dismissable: parseBoolean(dismissable), targetAccessLevels: JSON.parse(targetAccessLevels), targetPath, startsAt: new Date(startsAt), endsAt: new Date(endsAt), + showInCli: parseBoolean(showInCli), }, }, }); diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 422ec27346e..8c7612f37ff 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -215,7 +215,7 @@ export default { <form role="search" :aria-label="$options.i18n.SEARCH_GITLAB" - class="header-search gl-relative gl-rounded-base gl-w-full" + class="header-search-form gl-relative gl-rounded-base gl-w-full" :class="searchBarClasses" data-testid="header-search-form" > diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js index 766d4066e6a..2bbad5f3f98 100644 --- a/app/assets/javascripts/header_search/index.js +++ b/app/assets/javascripts/header_search/index.js @@ -16,7 +16,7 @@ export const initHeaderSearchApp = (search = '') => { } const searchContainer = headerEl.querySelector('.global-search-container'); - const newHeader = headerEl.querySelector('.header-search-new'); + const newHeader = headerEl.querySelector('.header-search'); const { searchPath, issuesPath, mrPath, autocompletePath } = el.dataset; let { searchContext } = el.dataset; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0c53b3fd866..b2ba1d8830d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -76,7 +76,7 @@ $search-input-field-x-min-width: 200px; } } - .header-search { + .header-search-form { min-width: $search-input-field-min-width; // This is a temporary workaround! diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index bbcc830353f..11eb39a11cb 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -826,15 +826,15 @@ kbd { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { margin: 0 2px; } -.navbar-gitlab .header-search { +.navbar-gitlab .header-search-form { min-width: 320px; } @media (min-width: 768px) and (max-width: 1199.98px) { - .navbar-gitlab .header-search { + .navbar-gitlab .header-search-form { min-width: 200px; } } -.navbar-gitlab .header-search .keyboard-shortcut-helper { +.navbar-gitlab .header-search-form .keyboard-shortcut-helper { transform: translateY(calc(50% - 2px)); box-shadow: none; border-color: transparent; @@ -1720,7 +1720,7 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav { body.gl-dark .navbar-gitlab .nav > li { color: #ececef; } -body.gl-dark .navbar-gitlab .nav > li.header-search-new { +body.gl-dark .navbar-gitlab .nav > li.header-search { color: #ececef; } body.gl-dark .navbar-gitlab .nav > li > a .notification-dot { @@ -1757,25 +1757,25 @@ body.gl-dark .notification-dot { background-color: #ececef; } -body.gl-dark .header-search { +body.gl-dark .header-search-form { background-color: rgba(236, 236, 239, 0.2) !important; border-radius: 4px; } -body.gl-dark .header-search svg.gl-search-box-by-type-search-icon { +body.gl-dark .header-search-form svg.gl-search-box-by-type-search-icon { color: rgba(236, 236, 239, 0.8); } -body.gl-dark .header-search input { +body.gl-dark .header-search-form input { background-color: transparent; color: rgba(236, 236, 239, 0.8); box-shadow: inset 0 0 0 1px rgba(236, 236, 239, 0.4); } -body.gl-dark .header-search input::placeholder { +body.gl-dark .header-search-form input::placeholder { color: rgba(236, 236, 239, 0.8); } -body.gl-dark .header-search input:active::placeholder { +body.gl-dark .header-search-form input:active::placeholder { color: #737278; } -body.gl-dark .header-search .keyboard-shortcut-helper { +body.gl-dark .header-search-form .keyboard-shortcut-helper { color: #ececef; background-color: rgba(236, 236, 239, 0.2); } @@ -1799,11 +1799,11 @@ body.gl-dark .navbar-gitlab .navbar-nav li.active > button { color: var(--gl-text-color); background-color: var(--gray-200); } -body.gl-dark .navbar-gitlab .header-search { +body.gl-dark .navbar-gitlab .header-search-form { background-color: var(--gray-100) !important; box-shadow: inset 0 0 0 1px var(--border-color) !important; } -body.gl-dark .navbar-gitlab .header-search:active { +body.gl-dark .navbar-gitlab .header-search-form:active { background-color: var(--gray-100) !important; box-shadow: inset 0 0 0 1px var(--blue-200) !important; } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 44651a66fcb..28a35c82a59 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -826,15 +826,15 @@ kbd { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { margin: 0 2px; } -.navbar-gitlab .header-search { +.navbar-gitlab .header-search-form { min-width: 320px; } @media (min-width: 768px) and (max-width: 1199.98px) { - .navbar-gitlab .header-search { + .navbar-gitlab .header-search-form { min-width: 200px; } } -.navbar-gitlab .header-search .keyboard-shortcut-helper { +.navbar-gitlab .header-search-form .keyboard-shortcut-helper { transform: translateY(calc(50% - 2px)); box-shadow: none; border-color: transparent; diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss index 3a18f735217..030e41046d3 100644 --- a/app/assets/stylesheets/themes/dark_mode_overrides.scss +++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss @@ -261,7 +261,7 @@ body.gl-dark { } } - .header-search { + .header-search-form { background-color: var(--gray-100) !important; box-shadow: inset 0 0 0 1px var(--border-color) !important; diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index 6e46100dbb3..f841a9047cc 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -68,7 +68,7 @@ > li { color: $search-and-nav-links; - &.header-search-new { + &.header-search { color: $gray-900; } @@ -151,7 +151,7 @@ } } - .header-search { + .header-search-form { background-color: $search-and-nav-links-a20 !important; border-radius: $border-radius-default; diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss index a0cbec9a92b..9b7fc10e769 100644 --- a/app/assets/stylesheets/themes/theme_light_gray.scss +++ b/app/assets/stylesheets/themes/theme_light_gray.scss @@ -52,7 +52,7 @@ body { } } - .header-search { + .header-search-form { background-color: $white !important; box-shadow: inset 0 0 0 1px $border-color !important; border-radius: $border-radius-default; diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 821c3cc1635..7f85103816e 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -93,6 +93,7 @@ module Admin target_path broadcast_type dismissable + show_in_cli ], target_access_levels: []).reverse_merge!(target_access_levels: []) end end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 2f14c907b12..a62ffa144f1 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -95,7 +95,8 @@ module BroadcastMessagesHelper target_path: broadcast_message.target_path, starts_at: broadcast_message.starts_at.iso8601, ends_at: broadcast_message.ends_at.iso8601, - target_access_level_options: target_access_level_options.to_json + target_access_level_options: target_access_level_options.to_json, + show_in_cli: broadcast_message.show_in_cli.to_s } end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 733018160cd..fd7a8708683 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -22,6 +22,7 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord validates :ends_at, presence: true validates :broadcast_type, presence: true validates :target_access_levels, inclusion: { in: ALLOWED_TARGET_ACCESS_LEVELS } + validates :show_in_cli, allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } validates :color, allow_blank: true, color: true validates :font, allow_blank: true, color: true @@ -29,6 +30,8 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord attribute :color, default: '#E75E40' attribute :font, default: '#FFFFFF' + scope :current_and_future_messages, -> { where('ends_at > :now', now: Time.current).order_id_asc } + CACHE_KEY = 'broadcast_message_current_json' BANNER_CACHE_KEY = 'broadcast_message_current_banner_json' NOTIFICATION_CACHE_KEY = 'broadcast_message_current_notification_json' @@ -60,6 +63,10 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord end end + def current_show_in_cli_banner_messages + current_banner_messages.select(&:show_in_cli?) + end + def current_notification_messages(current_path: nil, user_access_level: nil) fetch_messages NOTIFICATION_CACHE_KEY, current_path, user_access_level do current_and_future_messages.notification @@ -72,10 +79,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord end end - def current_and_future_messages - where('ends_at > :now', now: Time.current).order_id_asc - end - def cache ::Gitlab::SafeRequestStore.fetch(:broadcast_message_json_cache) do Gitlab::JsonCache.new diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index ef31bedc3a8..f9fa4bd212c 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -10,15 +10,19 @@ class DeployKey < Key has_many :projects, through: :deploy_keys_projects has_many :deploy_keys_projects_with_write_access, -> { with_write_access }, class_name: "DeployKeysProject", inverse_of: :deploy_key + has_many :deploy_keys_projects_with_readonly_access, -> { with_readonly_access }, class_name: "DeployKeysProject", inverse_of: :deploy_key has_many :projects_with_write_access, -> { includes(:route) }, class_name: 'Project', through: :deploy_keys_projects_with_write_access, source: :project + has_many :projects_with_readonly_access, -> { includes(:route) }, class_name: 'Project', through: :deploy_keys_projects_with_readonly_access, source: :project has_many :protected_branch_push_access_levels, class_name: '::ProtectedBranch::PushAccessLevel', inverse_of: :deploy_key has_many :protected_tag_create_access_levels, class_name: '::ProtectedTag::CreateAccessLevel', inverse_of: :deploy_key scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where(deploy_keys_projects: { project_id: projects }) } scope :with_write_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_write_access) } + scope :with_readonly_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_readonly_access) } scope :are_public, -> { where(public: true) } scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) } scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) } + scope :including_projects_with_readonly_access, -> { includes(:projects_with_readonly_access) } accepts_nested_attributes_for :deploy_keys_projects, reject_if: :reject_deploy_keys_projects? diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index 363ef0b1c9a..e114b7297eb 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -5,6 +5,7 @@ class DeployKeysProject < ApplicationRecord belongs_to :deploy_key, inverse_of: :deploy_keys_projects scope :in_project, ->(project) { where(project: project) } scope :with_write_access, -> { where(can_push: true) } + scope :with_readonly_access, -> { where(can_push: false) } accepts_nested_attributes_for :deploy_key diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb index c376b4036f8..5ab5732ecf5 100644 --- a/app/services/post_receive_service.rb +++ b/app/services/post_receive_service.rb @@ -86,14 +86,15 @@ class PostReceiveService banner = nil if project - scoped_messages = BroadcastMessage.current_banner_messages(current_path: project.full_path).select do |message| - message.target_path.present? && message.matches_current_path(project.full_path) - end + scoped_messages = + BroadcastMessage.current_banner_messages(current_path: project.full_path).select do |message| + message.target_path.present? && message.matches_current_path(project.full_path) && message.show_in_cli? + end banner = scoped_messages.last end - banner ||= BroadcastMessage.current_banner_messages.last + banner ||= BroadcastMessage.current_show_in_cli_banner_messages.last banner&.message end diff --git a/app/views/layouts/_header_search.html.haml b/app/views/layouts/_header_search.html.haml index 28118cf4aaa..1d5f2583bbd 100644 --- a/app/views/layouts/_header_search.html.haml +++ b/app/views/layouts/_header_search.html.haml @@ -1,4 +1,4 @@ -#js-header-search.header-search.is-not-active.gl-relative.gl-w-full{ data: { 'search-context' => header_search_context.to_json, +#js-header-search.header-search-form.is-not-active.gl-relative.gl-w-full{ data: { 'search-context' => header_search_context.to_json, 'search-path' => search_path, 'issues-path' => issues_dashboard_path, 'mr-path' => merge_requests_dashboard_path, diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 7156a0e5931..2c6ccb4abaf 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -29,7 +29,7 @@ .navbar-collapse.gl-transition-medium.collapse.gl-mr-auto.global-search-container.hide-when-top-nav-responsive-open - search_menu_item = top_nav_search_menu_item_attrs %ul.nav.navbar-nav.gl-w-full.gl-align-items-center - %li.nav-item.header-search-new.gl-display-none.gl-lg-display-block.gl-w-full + %li.nav-item.header-search.gl-display-none.gl-lg-display-block.gl-w-full - unless current_controller?(:search) = render 'layouts/header_search' %li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' } diff --git a/db/migrate/20230529163335_add_show_in_cli_to_broadcast_message.rb b/db/migrate/20230529163335_add_show_in_cli_to_broadcast_message.rb new file mode 100644 index 00000000000..3529b3d84e5 --- /dev/null +++ b/db/migrate/20230529163335_add_show_in_cli_to_broadcast_message.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddShowInCliToBroadcastMessage < Gitlab::Database::Migration[2.1] + def change + add_column :broadcast_messages, :show_in_cli, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20230529163335 b/db/schema_migrations/20230529163335 new file mode 100644 index 00000000000..5a165b81efa --- /dev/null +++ b/db/schema_migrations/20230529163335 @@ -0,0 +1 @@ +91e92552fb6283fd1d796c057418bf1f8346f7834eea324aab0fa82f0277c51b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9856a06a66f..7822973b778 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12761,7 +12761,8 @@ CREATE TABLE broadcast_messages ( broadcast_type smallint DEFAULT 1 NOT NULL, dismissable boolean, target_access_levels integer[] DEFAULT '{}'::integer[] NOT NULL, - theme smallint DEFAULT 0 NOT NULL + theme smallint DEFAULT 0 NOT NULL, + show_in_cli boolean DEFAULT true NOT NULL ); CREATE SEQUENCE broadcast_messages_id_seq diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 61e93b78067..003a5963ada 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -13,6 +13,8 @@ The deploy keys API can return in responses fingerprints of the public key in th ## List all deploy keys **(FREE SELF)** +> `projects_with_readonly_access` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119147) in GitLab 16.0. + Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires administrator access and is not available on GitLab.com. @@ -63,7 +65,8 @@ Example response: "path_with_namespace": "sidney_jones/project3", "created_at": "2021-10-25T18:33:17.666Z" } - ] + ], + "projects_with_readonly_access": [] }, { "id": 3, @@ -73,7 +76,18 @@ Example response: "fingerprint_sha256": "SHA256:lGI/Ys/Wx7PfMhUO1iuBH92JQKYN+3mhJZvWO4Q5ims", "created_at": "2013-10-02T11:12:29Z", "expires_at": null, - "projects_with_write_access": [] + "projects_with_write_access": [], + "projects_with_readonly_access": [ + { + "id": 74, + "description": null, + "name": "project3", + "name_with_namespace": "Sidney Jones / project3", + "path": "project3", + "path_with_namespace": "sidney_jones/project3", + "created_at": "2021-10-25T18:33:17.666Z" + } + ] } ] ``` diff --git a/doc/user/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md index acb3e92fff8..fe5628e3737 100644 --- a/doc/user/admin_area/broadcast_messages.md +++ b/doc/user/admin_area/broadcast_messages.md @@ -19,7 +19,7 @@ Broadcast messages can be managed using the [broadcast messages API](../../api/b ## Banners -Banners are shown on the top of a page and in Git remote responses. +Banners are shown on the top of a page and optionally in the command line as a Git remote response. ![Broadcast Message Banner](img/broadcast_messages_banner_v15_0.png) @@ -32,7 +32,7 @@ remote: ... ``` -If more than one banner is active at one time, they are displayed in a stack in order of creation. +If more than one banner is active at one time, they are displayed at the top of the page in order of creation. In the command line, only the latest banner is shown. ## Notifications @@ -69,6 +69,7 @@ To add a broadcast message: - `text-decoration` 1. Select a **Theme**. The default theme is `indigo`. 1. Select the **Dismissable** checkbox to enable users to dismiss the broadcast message. +1. Optional. Clear the **Git remote responses** checkbox to prevent broadcast messages from being displayed in the command line as Git remote responses. 1. Optional. Select **Target roles** to only show the broadcast message to users with the selected roles. The message displays on group, subgroup, and project pages, and does not display in Git remote responses. 1. If required, add a **Target Path** to only show the broadcast message on URLs matching that path. You can use the wildcard character `*` to match multiple URLs, for example `mygroup/myproject*`. 1. Select a date and time (UTC) for the message to start and end. diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 634d6052b99..f8379392531 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -41,8 +41,10 @@ module API authenticated_as_admin! deploy_keys = params[:public] ? DeployKey.are_public : DeployKey.all + deploy_keys = deploy_keys.including_projects_with_write_access.including_projects_with_readonly_access - present paginate(deploy_keys.including_projects_with_write_access), with: Entities::DeployKey, include_projects_with_write_access: true + present paginate(deploy_keys), + with: Entities::DeployKey, include_projects_with_write_access: true, include_projects_with_readonly_access: true end params do diff --git a/lib/api/entities/deploy_key.rb b/lib/api/entities/deploy_key.rb index 1bcd06f2c88..0e82d9abb63 100644 --- a/lib/api/entities/deploy_key.rb +++ b/lib/api/entities/deploy_key.rb @@ -14,6 +14,7 @@ module API documentation: { type: 'string', example: 'SHA256:Jrs3LD1Ji30xNLtTVf9NDCj7kkBgPBb2pjvTZ3HfIgU' } expose :projects_with_write_access, using: Entities::ProjectIdentity, if: -> (_, options) { options[:include_projects_with_write_access] } + expose :projects_with_readonly_access, using: Entities::ProjectIdentity, if: -> (_, options) { options[:include_projects_with_readonly_access] } end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3634d963636..44f1a66c33b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8227,6 +8227,9 @@ msgstr "" msgid "BroadcastMessages|Ends at" msgstr "" +msgid "BroadcastMessages|Git remote responses" +msgstr "" + msgid "BroadcastMessages|Green" msgstr "" @@ -8260,6 +8263,9 @@ msgstr "" msgid "BroadcastMessages|Red" msgstr "" +msgid "BroadcastMessages|Show the broadcast message in a command-line interface as a Git remote response" +msgstr "" + msgid "BroadcastMessages|Starts at" msgstr "" diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index fa8d255ae79..0602ce31136 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -5,6 +5,7 @@ FactoryBot.define do message { "MyText" } starts_at { 1.day.ago } ends_at { 1.day.from_now } + show_in_cli { true } broadcast_type { :banner } diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb index 2a429bf8e56..11833691329 100644 --- a/spec/factories/deploy_keys_projects.rb +++ b/spec/factories/deploy_keys_projects.rb @@ -8,5 +8,9 @@ FactoryBot.define do trait :write_access do can_push { true } end + + trait :readonly_access do + can_push { false } + end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_key.json b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json index 4f8b5c8422e..77e533488f0 100644 --- a/spec/fixtures/api/schemas/public_api/v4/deploy_key.json +++ b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json @@ -50,6 +50,12 @@ "items": { "$ref": "project/identity.json" } + }, + "projects_with_readonly_access": { + "type": "array", + "items": { + "$ref": "project/identity.json" + } } }, "additionalProperties": false diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js index ad5f4f6c3c1..dca77e67cac 100644 --- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js +++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js @@ -36,6 +36,7 @@ describe('MessageForm', () => { const findSubmitButton = () => wrapper.findComponent('[data-testid=submit-button]'); const findCancelButton = () => wrapper.findComponent('[data-testid=cancel-button]'); const findForm = () => wrapper.findComponent(GlForm); + const findShowInCli = () => wrapper.findComponent('[data-testid=show-in-cli-checkbox]'); function createComponent({ broadcastMessage = {} } = {}) { wrapper = mount(MessageForm, { @@ -99,6 +100,18 @@ describe('MessageForm', () => { }); }); + describe('showInCli checkbox', () => { + it('renders for Banners', () => { + createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } }); + expect(findShowInCli().exists()).toBe(true); + }); + + it('does not render for Notifications', () => { + createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } }); + expect(findShowInCli().exists()).toBe(false); + }); + }); + describe('target roles checkboxes', () => { it('renders target roles', () => { createComponent(); diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js index 9ccc6919b81..baf3c6f08b2 100644 --- a/spec/frontend/header_search/init_spec.js +++ b/spec/frontend/header_search/init_spec.js @@ -8,7 +8,7 @@ describe('Header Search EventListener', () => { jest.restoreAllMocks(); setHTMLFixture(` <div class="js-header-content"> - <div class="header-search" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search"> + <div class="header-search-form" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search"> <input autocomplete="off" class="form-control gl-form-input gl-search-box-by-type-input" data-qa-selector="search_box" id="search" name="search" placeholder="Search GitLab" type="text"> </div> </div>`); diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index 5d6d404d24d..05e745e249e 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -173,7 +173,7 @@ RSpec.describe BroadcastMessagesHelper, feature_category: :onboarding do it 'returns the expected message data attributes' do keys = [ :id, :message, :broadcast_type, :theme, :dismissable, :target_access_levels, :messages_path, - :preview_path, :target_path, :starts_at, :ends_at, :target_access_level_options + :preview_path, :target_path, :starts_at, :ends_at, :target_access_level_options, :show_in_cli ] expect(broadcast_message_data(message).keys).to match(keys) diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 5fcf6813b0a..13653dc95ff 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BroadcastMessage do +RSpec.describe BroadcastMessage, feature_category: :onboarding do subject { build(:broadcast_message) } it { is_expected.to be_valid } @@ -24,11 +24,17 @@ RSpec.describe BroadcastMessage do it { is_expected.to allow_value(1).for(:broadcast_type) } it { is_expected.not_to allow_value(nil).for(:broadcast_type) } it { is_expected.not_to allow_value(nil).for(:target_access_levels) } + it { is_expected.not_to allow_value(nil).for(:show_in_cli) } it do is_expected.to validate_inclusion_of(:target_access_levels) .in_array(described_class::ALLOWED_TARGET_ACCESS_LEVELS) end + + it do + is_expected.to validate_inclusion_of(:show_in_cli) + .in_array([true, false]) + end end describe 'default values' do @@ -331,6 +337,18 @@ RSpec.describe BroadcastMessage do end end + describe '.current_show_in_cli_banner_messages', :use_clean_rails_memory_store_caching do + subject { -> { described_class.current_show_in_cli_banner_messages } } + + it 'only returns banner messages that has show_in_cli as true' do + show_in_cli_message = create(:broadcast_message) + create(:broadcast_message, broadcast_type: :notification) + create(:broadcast_message, show_in_cli: false) + + expect(subject.call).to contain_exactly(show_in_cli_message) + end + end + describe '#attributes' do it 'includes message_html field' do expect(subject.attributes.keys).to include("cached_markdown_version", "message_html") @@ -404,4 +422,14 @@ RSpec.describe BroadcastMessage do message.flush_redis_cache end end + + describe '#current_and_future_messages' do + let_it_be(:message_a) { create(:broadcast_message, ends_at: 1.day.ago) } + let_it_be(:message_b) { create(:broadcast_message, ends_at: Time.current + 2.days) } + let_it_be(:message_c) { create(:broadcast_message, ends_at: Time.current + 7.days) } + + it 'returns only current and future messages by ascending ends_at' do + expect(described_class.current_and_future_messages).to eq [message_b, message_c] + end + end end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 337fa40b4ba..528b36babc6 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -20,6 +20,20 @@ RSpec.describe DeployKey, :mailer do .source(:project) end + it do + is_expected.to have_many(:deploy_keys_projects_with_readonly_access) + .conditions(can_push: false) + .class_name('DeployKeysProject') + .inverse_of(:deploy_key) + end + + it do + is_expected.to have_many(:projects_with_readonly_access) + .class_name('Project') + .through(:deploy_keys_projects_with_readonly_access) + .source(:project) + end + it { is_expected.to have_many(:projects) } it { is_expected.to have_many(:protected_branch_push_access_levels).inverse_of(:deploy_key) } it { is_expected.to have_many(:protected_tag_create_access_levels).inverse_of(:deploy_key) } @@ -95,7 +109,7 @@ RSpec.describe DeployKey, :mailer do it { is_expected.to be_empty } end - context 'and this deploy key has not write access to the project' do + context 'and this deploy key has no write access to the project' do let(:specific_deploy_key) { create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, project: project)]) } it { is_expected.to be_empty } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 18a9211df3e..30c345ef458 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -59,6 +59,17 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect { make_api_request }.not_to exceed_all_query_limit(control) end + it 'avoids N+1 database queries', :use_sql_query_cache, :request_store do + create(:deploy_keys_project, :readonly_access, project: project2, deploy_key: deploy_key) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { make_api_request } + + deploy_key2 = create(:deploy_key, public: true) + create(:deploy_keys_project, :readonly_access, project: project3, deploy_key: deploy_key2) + + expect { make_api_request }.not_to exceed_all_query_limit(control) + end + context 'when `public` parameter is `true`' do it 'only returns public deploy keys' do make_api_request({ public: true }) @@ -81,6 +92,21 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(response_projects_with_write_access[1]['id']).to eq(project3.id) end end + + context 'projects_with_readonly_access' do + let!(:deploy_keys_project2) { create(:deploy_keys_project, :readonly_access, project: project2, deploy_key: deploy_key) } + let!(:deploy_keys_project3) { create(:deploy_keys_project, :readonly_access, project: project3, deploy_key: deploy_key) } + + it 'returns projects with readonly access' do + make_api_request + + response_projects_with_readonly_access = json_response.first['projects_with_readonly_access'] + + expect(response_projects_with_readonly_access[0]['id']).to eq(project.id) + expect(response_projects_with_readonly_access[1]['id']).to eq(project2.id) + expect(response_projects_with_readonly_access[2]['id']).to eq(project3.id) + end + end end end @@ -103,6 +129,7 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) expect(json_response.first).not_to have_key(:projects_with_write_access) + expect(json_response.first).not_to have_key(:projects_with_readonly_access) end it 'returns multiple deploy keys without N + 1' do @@ -129,6 +156,7 @@ RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuo expect(json_response['title']).to eq(deploy_key.title) expect(json_response).not_to have_key(:projects_with_write_access) + expect(json_response).not_to have_key(:projects_with_readonly_access) end it 'returns 404 Not Found with invalid ID' do diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb index 13bd103003f..20d86f74f86 100644 --- a/spec/services/post_receive_service_spec.rb +++ b/spec/services/post_receive_service_spec.rb @@ -214,11 +214,17 @@ RSpec.describe PostReceiveService, feature_category: :team_planning do end context 'broadcast message banner exists' do - it 'outputs a broadcast message' do - broadcast_message = create(:broadcast_message) + it 'outputs a broadcast message when show_in_cli is true' do + broadcast_message = create(:broadcast_message, show_in_cli: true) expect(subject).to include(build_alert_message(broadcast_message.message)) end + + it 'does not output a broadcast message when show_in_cli is false' do + create(:broadcast_message, show_in_cli: false) + + expect(has_alert_messages?(subject)).to be_falsey + end end context 'broadcast message notification exists' do diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 75853371c0f..72673648b89 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -2,7 +2,7 @@ module SearchHelpers def fill_in_search(text) - page.within('.header-search-new') do + page.within('.header-search') do find('#search').click fill_in 'search', with: text end @@ -11,7 +11,7 @@ module SearchHelpers end def submit_search(query) - page.within('.header-search, .search-page-form') do + page.within('.header-search-form, .search-page-form') do field = find_field('search') field.click field.fill_in(with: query) |