diff options
55 files changed, 519 insertions, 222 deletions
diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml index 708944acbc7..1285fe6ef3f 100644 --- a/.rubocop_todo/layout/space_in_lambda_literal.yml +++ b/.rubocop_todo/layout/space_in_lambda_literal.yml @@ -277,7 +277,6 @@ Layout/SpaceInLambdaLiteral: - 'ee/lib/ee/api/entities/list.rb' - 'ee/lib/ee/api/entities/member.rb' - 'ee/lib/ee/api/entities/project_approval_rule.rb' - - 'ee/lib/ee/api/entities/user_basic.rb' - 'ee/lib/ee/api/entities/vulnerability_issue_link.rb' - 'ee/lib/ee/gitlab/background_migration/backfill_epic_cache_counts.rb' - 'ee/lib/ee/gitlab/background_migration/delete_approval_rules_with_vulnerability.rb' diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index ee1a7633a11..21456564d3b 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -1,12 +1,14 @@ -import $ from 'jquery'; import initVueAlerts from '~/vue_alerts'; import NoEmojiValidator from '~/emoji/no_emoji_validator'; import { initLanguageSwitcher } from '~/language_switcher'; import LengthValidator from '~/validators/length_validator'; import mountEmailVerificationApplication from '~/sessions/new'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; -import OAuthRememberMe from './oauth_remember_me'; -import preserveUrlFragment from './preserve_url_fragment'; +import { + appendUrlFragment, + appendRedirectQuery, + toggleRememberMeQuery, +} from './preserve_url_fragment'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import UsernameValidator from './username_validator'; @@ -15,13 +17,9 @@ new LengthValidator(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new -new OAuthRememberMe({ - container: $('.js-oauth-login'), -}).bindEvents(); - -// Save the URL fragment from the current window location. This will be present if the user was -// redirected to sign-in after attempting to access a protected URL that included a fragment. -preserveUrlFragment(window.location.hash); +appendUrlFragment(); +appendRedirectQuery(); +toggleRememberMeQuery(); initVueAlerts(); initLanguageSwitcher(); mountEmailVerificationApplication(); diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js deleted file mode 100644 index 3336b094560..00000000000 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'jquery'; -import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; - -/** - * OAuth-based login buttons have a separate "remember me" checkbox. - * - * Toggling this checkbox adds/removes a `remember_me` parameter to the - * login buttons' parent form action, which is passed on to the omniauth callback. - */ - -export default class OAuthRememberMe { - constructor(opts = {}) { - this.container = opts.container || ''; - } - - bindEvents() { - $('#remember_me_omniauth', this.container).on('click', this.toggleRememberMe); - } - - toggleRememberMe(event) { - const rememberMe = $(event.target).is(':checked'); - - $('.js-oauth-login form', this.container).each((_, form) => { - const $form = $(form); - const href = $form.attr('action'); - - if (rememberMe) { - $form.attr('action', mergeUrlParams({ remember_me: 1 }, href)); - } else { - $form.attr('action', removeParams(['remember_me'], href)); - } - }); - } -} diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js index 54ec3c52f62..de48a457bcd 100644 --- a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js +++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js @@ -1,32 +1,72 @@ -import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility'; +import { mergeUrlParams, removeParams, setUrlFragment } from '~/lib/utils/url_utility'; /** - * Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and - * OAuth/SAML login links. + * Append the fragment to all non-OAuth login form actions so it is preserved + * when the user is eventually redirected back to the originally requested URL. * * @param fragment {string} - url fragment to be preserved */ -export default function preserveUrlFragment(fragment = '') { - if (fragment) { - const normalFragment = fragment.replace(/^#/, ''); - - // Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is - // eventually redirected back to the originally requested URL. - const forms = document.querySelectorAll('.js-non-oauth-login form'); - Array.prototype.forEach.call(forms, (form) => { - const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`); - form.setAttribute('action', actionWithFragment); - }); +export function appendUrlFragment(fragment = document.location.hash) { + if (!fragment) { + return; + } + + const normalFragment = fragment.replace(/^#/, ''); + const forms = document.querySelectorAll('.js-non-oauth-login form'); + forms.forEach((form) => { + const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`); + form.setAttribute('action', actionWithFragment); + }); +} + +/** + * Append a redirect_fragment query param to all OAuth login form actions. The + * redirect_fragment query param will be available in the omniauth callback upon + * successful authentication. + * + * @param {string} fragment - url fragment to be preserved + */ +export function appendRedirectQuery(fragment = document.location.hash) { + if (!fragment) { + return; + } + + const normalFragment = fragment.replace(/^#/, ''); + const oauthForms = document.querySelectorAll('.js-oauth-login form'); + oauthForms.forEach((oauthForm) => { + const newHref = mergeUrlParams( + { redirect_fragment: normalFragment }, + oauthForm.getAttribute('action'), + ); + oauthForm.setAttribute('action', newHref); + }); +} + +/** + * OAuth login buttons have a separate "remember me" checkbox. + * + * Toggling this checkbox adds/removes a `remember_me` parameter to the + * login form actions, which is passed on to the omniauth callback. + */ +export function toggleRememberMeQuery() { + const oauthForms = document.querySelectorAll('.js-oauth-login form'); + const checkbox = document.querySelector('#js-remember-me-omniauth'); + + if (oauthForms.length === 0 || !checkbox) { + return; + } + + checkbox.addEventListener('change', ({ currentTarget }) => { + oauthForms.forEach((oauthForm) => { + const href = oauthForm.getAttribute('action'); + let newHref; + if (currentTarget.checked) { + newHref = mergeUrlParams({ remember_me: '1' }, href); + } else { + newHref = removeParams(['remember_me'], href); + } - // Append a redirect_fragment query param to all oauth provider links. The redirect_fragment - // query param will be available in the omniauth callback upon successful authentication - const oauthForms = document.querySelectorAll('.js-oauth-login form'); - Array.prototype.forEach.call(oauthForms, (oauthForm) => { - const newHref = mergeUrlParams( - { redirect_fragment: normalFragment }, - oauthForm.getAttribute('action'), - ); oauthForm.setAttribute('action', newHref); }); - } + }); } diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue index 319ce2cea84..9b6b0f1cb2a 100644 --- a/app/assets/javascripts/repository/components/commit_info.vue +++ b/app/assets/javascripts/repository/components/commit_info.vue @@ -26,6 +26,11 @@ export default { type: Object, required: true, }, + span: { + type: Number, + required: false, + default: null, + }, prevBlameLink: { type: String, required: false, @@ -43,6 +48,9 @@ export default { avatarLinkAltText() { return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName }); }, + truncateAuthorName() { + return typeof this.span === 'number' && this.span < 3; + }, }, methods: { toggleShowDescription() { @@ -102,18 +110,23 @@ export default { @click="toggleShowDescription" /> </div> - <div class="committer gl-flex-basis-full"> + <div + class="committer gl-flex-basis-full" + :class="truncateAuthorName ? 'gl-display-inline-flex' : ''" + data-testid="committer" + > <gl-link v-if="commit.author" :href="commit.author.webPath" class="commit-author-link js-user-link" + :class="truncateAuthorName ? 'gl-display-inline-block gl-text-truncate' : ''" > {{ commit.author.name }}</gl-link > <template v-else> {{ commit.authorName }} </template> - {{ $options.i18n.authored }} + {{ $options.i18n.authored }} <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" /> </div> <pre diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js index 7da6da16d6f..d4469382be4 100644 --- a/app/assets/javascripts/tracking/internal_events.js +++ b/app/assets/javascripts/tracking/internal_events.js @@ -9,10 +9,13 @@ const InternalEvents = { /** * * @param {string} event + * @param {string} category - The category of the event. This is optional and + * defaults to the page name where the event was triggered. It's advised not to use + * this parameter for new events unless absolutely necessary. */ - trackEvent(event) { + trackEvent(event, category = undefined) { API.trackInternalEvent(event); - Tracking.event(undefined, event, { + Tracking.event(category, event, { context: { schema: SERVICE_PING_SCHEMA, data: { @@ -30,8 +33,8 @@ const InternalEvents = { mixin() { return { methods: { - trackEvent(event) { - InternalEvents.trackEvent(event); + trackEvent(event, category = undefined) { + InternalEvents.trackEvent(event, category); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue index e2fd4477f0a..3205ec2b73b 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue @@ -30,6 +30,7 @@ export default { class="gl-display-flex gl-absolute gl-px-3" :style="{ top: blame.blameOffset }" :commit="blame.commit" + :span="blame.span" :prev-blame-link="blame.commitData && blame.commitData.projectBlameLink" /> </div> diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index b0519119bd8..e455682dcff 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -136,6 +136,10 @@ .commit-author-link { color: $gl-text-color; } + + .commit-author-link.gl-text-truncate { + max-width: 20ch; + } } } diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb index 191720f69a0..8a3ec13f720 100644 --- a/app/controllers/groups/autocomplete_sources_controller.rb +++ b/app/controllers/groups/autocomplete_sources_controller.rb @@ -51,7 +51,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541 # type_id is a misnomer. QuickActions::TargetService actually requires an iid. QuickActions::TargetService - .new(nil, current_user, group: @group) + .new(container: @group, current_user: current_user) .execute(params[:type], params[:type_id]) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index dc10004c62b..c496a326051 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -59,7 +59,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/388541 # type_id is a misnomer. QuickActions::TargetService actually requires an iid. QuickActions::TargetService - .new(project, current_user) + .new(container: project, current_user: current_user) .execute(target_type, params[:type_id]) end diff --git a/app/graphql/resolvers/projects/is_forked_resolver.rb b/app/graphql/resolvers/projects/is_forked_resolver.rb new file mode 100644 index 00000000000..f1413543b7c --- /dev/null +++ b/app/graphql/resolvers/projects/is_forked_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class IsForkedResolver < BaseResolver + type GraphQL::Types::Boolean, null: false + + def resolve + lazy_fork_network_members = BatchLoader::GraphQL.for(object.id).batch do |ids, loader| + ForkNetworkMember.by_projects(ids) + .with_fork_network + .find_each do |fork_network_member| + loader.call(fork_network_member.project_id, fork_network_member) + end + end + + Gitlab::Graphql::Lazy.with_value(lazy_fork_network_members) do |fork_network_member| + next false if fork_network_member.nil? + + fork_network_member.fork_network.root_project_id != object.id + end + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index aacd67e269e..bedcd08fcb4 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -685,6 +685,12 @@ module Types description: 'Project allows assigning multiple reviewers to a merge request.', null: false + field :is_forked, + GraphQL::Types::Boolean, + resolver: Resolvers::Projects::IsForkedResolver, + description: 'Project is forked.', + null: false + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index e0e6906f211..05c9535cef1 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -10,6 +10,9 @@ module Ci include FileStoreMounter include Lockable include Presentable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id FILE_SIZE_LIMIT = 10.megabytes.freeze EXPIRATION_DATE = 1.week.freeze diff --git a/app/models/ci/pipeline_config.rb b/app/models/ci/pipeline_config.rb index 11decd3fc66..8e992aae2c5 100644 --- a/app/models/ci/pipeline_config.rb +++ b/app/models/ci/pipeline_config.rb @@ -3,6 +3,9 @@ module Ci class PipelineConfig < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.table_name = 'ci_pipelines_config' self.primary_key = :pipeline_id diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb index 21d102374f0..39e2ef5cebb 100644 --- a/app/models/ci/pipeline_metadata.rb +++ b/app/models/ci/pipeline_metadata.rb @@ -4,6 +4,9 @@ module Ci class PipelineMetadata < Ci::ApplicationRecord include Ci::Partitionable include Importable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.primary_key = :pipeline_id diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb index f18c306cf91..023f948d5f9 100644 --- a/app/models/fork_network_member.rb +++ b/app/models/fork_network_member.rb @@ -9,6 +9,9 @@ class ForkNetworkMember < ApplicationRecord after_destroy :cleanup_fork_network + scope :by_projects, ->(ids) { where(project_id: ids) } + scope :with_fork_network, -> { joins(:fork_network).includes(:fork_network) } + private def cleanup_fork_network diff --git a/app/models/user.rb b/app/models/user.rb index ab5572e5b19..05e35b217f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -631,6 +631,8 @@ class User < MainClusterwide::ApplicationRecord .trusted_with_spam) end + scope :preload_user_detail, -> { preload(:user_detail) } + def self.supported_keyset_orderings { id: [:asc, :desc], diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 10aef87332a..31f79bc7164 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -55,7 +55,7 @@ class PreviewMarkdownService < BaseService def find_commands_target QuickActions::TargetService - .new(project, current_user, group: params[:group]) + .new(container: project, current_user: current_user, params: { group: params[:group] }) .execute(target_type, target_id) end diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb index 04ae5287302..63e2c58fc55 100644 --- a/app/services/quick_actions/target_service.rb +++ b/app/services/quick_actions/target_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QuickActions - class TargetService < BaseService + class TargetService < BaseContainerService def execute(type, type_iid) case type&.downcase when 'workitem' @@ -19,15 +19,15 @@ module QuickActions # rubocop: disable CodeReuse/ActiveRecord def work_item(type_iid) - WorkItems::WorkItemsFinder.new(current_user, project_id: project.id).find_by(iid: type_iid) + WorkItems::WorkItemsFinder.new(current_user, **parent_params).find_by(iid: type_iid) end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def issue(type_iid) - return project.issues.build if type_iid.nil? + return container.issues.build if type_iid.nil? - IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_iid) || project.issues.build + IssuesFinder.new(current_user, **parent_params).find_by(iid: type_iid) || container.issues.build end # rubocop: enable CodeReuse/ActiveRecord @@ -42,7 +42,11 @@ module QuickActions def commit(type_iid) project.commit(type_iid) end + + def parent_params + group_container? ? { group_id: group.id } : { project_id: project.id } + end end end -QuickActions::TargetService.prepend_mod_with('QuickActions::TargetService') +QuickActions::TargetService.prepend_mod diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 8197abcc787..5fbb20f7535 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -12,6 +12,6 @@ data: { testid: test_id_for_provider(provider) }, id: "oauth-login-#{provider}" - if render_remember_me - = render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c| + = render Pajamas::CheckboxTagComponent.new(name: 'js-remember-me-omniauth', value: nil) do |c| - c.with_label do = _('Remember me') diff --git a/db/post_migrate/20240123131916_remove_partition_id_default_value_for_ci_pipeline_metadata.rb b/db/post_migrate/20240123131916_remove_partition_id_default_value_for_ci_pipeline_metadata.rb new file mode 100644 index 00000000000..a2a0cc7be87 --- /dev/null +++ b/db/post_migrate/20240123131916_remove_partition_id_default_value_for_ci_pipeline_metadata.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemovePartitionIdDefaultValueForCiPipelineMetadata < Gitlab::Database::Migration[2.2] + milestone '16.9' + enable_lock_retries! + + TABLE_NAME = :ci_pipeline_metadata + COLUM_NAME = :partition_id + + def change + change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil) + end +end diff --git a/db/post_migrate/20240123132014_remove_partition_id_default_value_for_ci_pipeline_artifact.rb b/db/post_migrate/20240123132014_remove_partition_id_default_value_for_ci_pipeline_artifact.rb new file mode 100644 index 00000000000..205c7b19db6 --- /dev/null +++ b/db/post_migrate/20240123132014_remove_partition_id_default_value_for_ci_pipeline_artifact.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemovePartitionIdDefaultValueForCiPipelineArtifact < Gitlab::Database::Migration[2.2] + milestone '16.9' + enable_lock_retries! + + TABLE_NAME = :ci_pipeline_artifacts + COLUM_NAME = :partition_id + + def change + change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil) + end +end diff --git a/db/post_migrate/20240123132048_remove_partition_id_default_value_for_ci_pipeline_config.rb b/db/post_migrate/20240123132048_remove_partition_id_default_value_for_ci_pipeline_config.rb new file mode 100644 index 00000000000..b0a92807f09 --- /dev/null +++ b/db/post_migrate/20240123132048_remove_partition_id_default_value_for_ci_pipeline_config.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemovePartitionIdDefaultValueForCiPipelineConfig < Gitlab::Database::Migration[2.2] + milestone '16.9' + enable_lock_retries! + + TABLE_NAME = :ci_pipelines_config + COLUM_NAME = :partition_id + + def change + change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil) + end +end diff --git a/db/schema_migrations/20240123131916 b/db/schema_migrations/20240123131916 new file mode 100644 index 00000000000..5377f7f4fb9 --- /dev/null +++ b/db/schema_migrations/20240123131916 @@ -0,0 +1 @@ +43ff332582062a104cef5449444034363c1a71d288bcae7dfdeefbd69500186e
\ No newline at end of file diff --git a/db/schema_migrations/20240123132014 b/db/schema_migrations/20240123132014 new file mode 100644 index 00000000000..719730631bd --- /dev/null +++ b/db/schema_migrations/20240123132014 @@ -0,0 +1 @@ +29392953f2fce7fb1a24dbc49f1ea30c49b1006551599bff98edc4de8061106b
\ No newline at end of file diff --git a/db/schema_migrations/20240123132048 b/db/schema_migrations/20240123132048 new file mode 100644 index 00000000000..a8a046d0a04 --- /dev/null +++ b/db/schema_migrations/20240123132048 @@ -0,0 +1 @@ +d14905475e591b7fa855097434d0e810fbb5a0890d7feb7b4fe8a22d5d75335f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index cd9e8be6d35..7631fecfe7a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14603,7 +14603,7 @@ CREATE TABLE ci_pipeline_artifacts ( verification_checksum bytea, verification_failure text, locked smallint DEFAULT 2, - partition_id bigint DEFAULT 100 NOT NULL, + partition_id bigint NOT NULL, CONSTRAINT check_191b5850ec CHECK ((char_length(file) <= 255)), CONSTRAINT check_abeeb71caf CHECK ((file IS NOT NULL)), CONSTRAINT ci_pipeline_artifacts_verification_failure_text_limit CHECK ((char_length(verification_failure) <= 255)) @@ -14658,7 +14658,7 @@ CREATE TABLE ci_pipeline_metadata ( name text, auto_cancel_on_new_commit smallint DEFAULT 0 NOT NULL, auto_cancel_on_job_failure smallint DEFAULT 0 NOT NULL, - partition_id bigint DEFAULT 100 NOT NULL, + partition_id bigint NOT NULL, CONSTRAINT check_9d3665463c CHECK ((char_length(name) <= 255)) ); @@ -14782,7 +14782,7 @@ CREATE TABLE ci_pipelines ( CREATE TABLE ci_pipelines_config ( pipeline_id bigint NOT NULL, content text NOT NULL, - partition_id bigint DEFAULT 100 NOT NULL + partition_id bigint NOT NULL ); CREATE SEQUENCE ci_pipelines_id_seq diff --git a/doc/.vale/gitlab/Substitutions.yml b/doc/.vale/gitlab/Substitutions.yml index 0d49ac583dd..26caf353314 100644 --- a/doc/.vale/gitlab/Substitutions.yml +++ b/doc/.vale/gitlab/Substitutions.yml @@ -28,8 +28,10 @@ swap: raketask: Rake task raketasks: Rake tasks rspec: RSpec - self hosted: self-managed - self-hosted: self-managed + GitLab self hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed + GitLab self-hosted: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed + self hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed + self-hosted GitLab: GitLab self-managed # https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#gitlab-self-managed styleguide: style guide to login: to log in can login: can log in diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 0f2f16ffc45..b21e82c9b54 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -24351,6 +24351,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. | | <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. | | <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 15.11. This feature is an Experiment. It can be changed or removed at any time. Indicates if a project is a catalog resource. | +| <a id="projectisforked"></a>`isForked` | [`Boolean!`](#boolean) | Project is forked. | | <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. | | <a id="projectissuesenabled"></a>`issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user. | | <a id="projectjiraimportstatus"></a>`jiraImportStatus` | [`String`](#string) | Status of Jira import background job of the project. | diff --git a/doc/api/members.md b/doc/api/members.md index 9d7aa85ba93..ead5b3d6be7 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -29,8 +29,7 @@ In GitLab 14.8 and earlier, projects in personal namespaces have an `access_leve The `group_saml_identity` attribute is only visible to group owners for [SSO-enabled groups](../user/group/saml_sso/index.md). -The `email` attribute is only visible to group owners for users provisioned by the group with [SCIM](../user/group/saml_sso/scim_setup.md). -[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to the `email` attribute from provisioned users to [enterprise users](../user/enterprise_user/index.md). +The `email` attribute is only visible to group owners for [enterprise users](../user/enterprise_user/index.md) of the group when an API request is sent to the group itself, or that group's subgroups or projects. ## List all members of a group or project diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 5854704521b..eafb31241b0 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -252,7 +252,7 @@ Ideally, you should use [CI/CD variables](../variables/predefined_variables.md) to replace those values at runtime when each review app is created: - `data-project-id` is the project ID, which can be found by the `CI_PROJECT_ID` - variable. + variable or on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id). - `data-merge-request-id` is the merge request ID, which can be found by the `CI_MERGE_REQUEST_IID` variable. `CI_MERGE_REQUEST_IID` is available only if [`rules:if: $CI_PIPELINE_SOURCE == "merge_request_event`](../pipelines/merge_request_pipelines.md#use-rules-to-add-jobs) diff --git a/doc/ci/triggers/index.md b/doc/ci/triggers/index.md index 49ff0ee2356..b628159ad21 100644 --- a/doc/ci/triggers/index.md +++ b/doc/ci/triggers/index.md @@ -78,7 +78,7 @@ In each example, replace: - `<token>` with your trigger token. - `<ref_name>` with a branch or tag name, like `main`. - `<project_id>` with your project ID, like `123456`. The project ID is displayed - at the top of every project's landing page. + on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id). ### Use a CI/CD job @@ -100,8 +100,8 @@ trigger_pipeline: In this example: -- `1234` is the project ID for `project-B`. The project ID is displayed at the top - of every project's landing page. +- `1234` is the project ID for `project-B`. The project ID is displayed on the + [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id). - The [`rules`](../yaml/index.md#rules) cause the job to run every time a tag is added to `project-A`. - `MY_TRIGGER_TOKEN` is a [masked CI/CD variables](../variables/index.md#mask-a-cicd-variable) that contains the trigger token. @@ -119,7 +119,7 @@ Replace: - The URL with `https://gitlab.com` or the URL of your instance. - `<project_id>` with your project ID, like `123456`. The project ID is displayed - at the top of the project's landing page. + on the [project overview page](../../user/project/working_with_projects.md#access-the-project-overview-page-by-using-the-project-id). - `<ref_name>` with a branch or tag name, like `main`. This value takes precedence over the `ref_name` in the webhook payload. The payload's `ref` is the branch that fired the trigger in the source repository. You must URL-encode the `ref_name` if it contains slashes. diff --git a/doc/development/internal_analytics/internal_event_instrumentation/migration.md b/doc/development/internal_analytics/internal_event_instrumentation/migration.md index 79ca45ed84c..7ed1adfb187 100644 --- a/doc/development/internal_analytics/internal_event_instrumentation/migration.md +++ b/doc/development/internal_analytics/internal_event_instrumentation/migration.md @@ -21,7 +21,7 @@ If you are already tracking events in Snowplow, you can also start collecting me The event triggered by Internal Events has some special properties compared to previously tracking with Snowplow directly: 1. The `label`, `property` and `value` attributes are not used within Internal Events and are always empty. -1. The `category` is automatically set to `InternalEventTracking` +1. The `category` is automatically set to the location where the event happened. For Frontend events it is the page name and for Backend events it is a class name. If the page name or class name is not used, the default value of `"InternalEventTracking"` will be used. Make sure that you are okay with this change before you migrate and dashboards are changed accordingly. @@ -73,9 +73,11 @@ import { InternalEvents } from '~/tracking'; mixins: [InternalEvents.mixin()] ... ... -this.trackEvent('action') +this.trackEvent('action', 'category') ``` +If you are currently passing `category` and need to keep it, it can be passed as the second argument in the `trackEvent` method, as illustrated in the previous example. Nonetheless, it is strongly advised against using the `category` parameter for new events. This is because, by default, the category field is populated with information about where the event was triggered. + You can use [this MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123901/diffs) as an example. It migrates the `devops_adoption_app` component to use Internal Events Tracking. If you are using `data-track-action` in the component, you have to change it to `data-event-tracking` to migrate to Internal Events Tracking. diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index e0a97568b5b..2a8a0766323 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -81,11 +81,10 @@ For more information about our plans for language support in SAST, see the [cate | TypeScript | [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep/#sast-rules) | 13.10 | <html> -<small>Footnotes: + Footnotes: <ol> - <li>The SpotBugs-based analyzer supports [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/). It can also be used with variants like the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), [Grails](https://grails.org/), and the [Maven wrapper](https://github.com/takari/maven-wrapper). However, SpotBugs has [limitations](https://gitlab.com/gitlab-org/gitlab/-/issues/350801) when used against [Ant](https://ant.apache.org/)-based projects. We recommend using the Semgrep-based analyzer for Ant-based Java or Scala projects.</li> + <li>The SpotBugs-based analyzer supports <a href="https://gradle.org/">Gradle</a>, <a href="https://maven.apache.org/">Maven</a>, and <a href="https://www.scala-sbt.org/">SBT</a>. It can also be used with variants like the <a href="https://docs.gradle.org/current/userguide/gradle_wrapper.html">Gradle wrapper</a>, <a href="https://grails.org/">Grails</a>, and the <a href="https://github.com/takari/maven-wrapper">Maven wrapper</a>. However, SpotBugs has <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/350801">limitations</a> when used against <a href="https://ant.apache.org/">Ant</a>-based projects. You should use the Semgrep-based analyzer for Ant-based Java or Scala projects.</li> </ol> -</small> </html> ## End of supported analyzers diff --git a/doc/user/enterprise_user/index.md b/doc/user/enterprise_user/index.md index cc7e0e3b499..5a6ee56b775 100644 --- a/doc/user/enterprise_user/index.md +++ b/doc/user/enterprise_user/index.md @@ -203,10 +203,7 @@ A top-level group Owner can [set up verified domains to bypass confirmation emai ### Get users' email addresses through the API A top-level group Owner can use the [group and project members API](../../api/members.md) to access -users' information. For users provisioned by the group with [SCIM](../group/saml_sso/scim_setup.md), -this information includes users' email addresses. - -[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to email addresses from provisioned users to enterprise users. +users' information. For enterprise users of the group this information includes users' email addresses. ### Remove enterprise management features from an account diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a59734d643d..9b2c6c37fd6 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -738,6 +738,12 @@ module API namespace: namespace, project: project ) + rescue Gitlab::InternalEvents::UnknownEventError => e + Gitlab::ErrorTracking.track_exception(e, event_name: event_name) + + # We want to keep the error silent on production to keep the behavior + # consistent with StandardError rescue + unprocessable_entity!(e.message) if Gitlab.dev_or_test_env? rescue StandardError => e Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, event_name: event_name) end diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index d625b2c0fe6..09bb336e19c 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -73,7 +73,7 @@ module API end desc 'Updates a group or project invitation.' do - success Entities::Member + success Entities::Invitation tags %w[invitations] end params do @@ -103,7 +103,7 @@ module API updated_member = result[:members].first if result[:status] == :success - present_members updated_member + present_member_invitations updated_member else render_validation_error!(updated_member) end diff --git a/lib/bulk_imports/common/pipelines/members_pipeline.rb b/lib/bulk_imports/common/pipelines/members_pipeline.rb index 548b191dc25..90df8453d77 100644 --- a/lib/bulk_imports/common/pipelines/members_pipeline.rb +++ b/lib/bulk_imports/common/pipelines/members_pipeline.rb @@ -27,7 +27,9 @@ module BulkImports return if user_membership && user_membership[:access_level] >= data[:access_level] # Create new membership for any other access level - portable.members.create!(data) + member = portable.members.new(data) + member.importing = true # avoid sending new member notification to the invited user + member.save! end private diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c1b1daf08e9..dad32def094 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2918,6 +2918,9 @@ msgstr "" msgid "Add an impersonation token" msgstr "" +msgid "Add another branch" +msgstr "" + msgid "Add another link" msgstr "" @@ -44054,6 +44057,9 @@ msgstr "" msgid "SecurityOrchestration|Add new approver" msgstr "" +msgid "SecurityOrchestration|Add project full path after @ to following branches: %{branches}" +msgstr "" + msgid "SecurityOrchestration|Add protected branches" msgstr "" @@ -44123,6 +44129,9 @@ msgstr "" msgid "SecurityOrchestration|Choose approver type" msgstr "" +msgid "SecurityOrchestration|Choose exception branches" +msgstr "" + msgid "SecurityOrchestration|Choose specific role" msgstr "" @@ -44192,6 +44201,9 @@ msgstr "" msgid "SecurityOrchestration|Every time a pipeline runs for %{branches}%{branchExceptionsString}" msgstr "" +msgid "SecurityOrchestration|Exception branches" +msgstr "" + msgid "SecurityOrchestration|Exceptions" msgstr "" @@ -44207,6 +44219,9 @@ msgstr "" msgid "SecurityOrchestration|Failed to load images." msgstr "" +msgid "SecurityOrchestration|Fill in branch name with project name in the format of %{boldStart}branch-name@project-path,%{boldEnd} separate with `,`" +msgstr "" + msgid "SecurityOrchestration|Following projects:" msgstr "" @@ -44276,6 +44291,9 @@ msgstr "" msgid "SecurityOrchestration|No actions defined - policy will not run." msgstr "" +msgid "SecurityOrchestration|No branches yet" +msgstr "" + msgid "SecurityOrchestration|No compliance frameworks" msgstr "" @@ -44320,6 +44338,9 @@ msgstr "" msgid "SecurityOrchestration|Overwrite the current CI/CD code with the new file's content?" msgstr "" +msgid "SecurityOrchestration|Please remove duplicated values" +msgstr "" + msgid "SecurityOrchestration|Policies" msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index 72ce6cfe43c..ec053724906 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'gitlab-qa', '~> 13', '>= 13.1.0', require: 'gitlab/qa' +gem 'gitlab-qa', '~> 14', require: 'gitlab/qa' gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false gem 'gitlab-utils', path: '../gems/gitlab-utils' gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 126061e83cb..26dae330ef6 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -118,8 +118,8 @@ GEM gitlab (4.19.0) httparty (~> 0.20) terminal-table (>= 1.5.1) - gitlab-qa (13.1.0) - activesupport (>= 6.1, < 7.1) + gitlab-qa (14.0.0) + activesupport (>= 6.1, < 7.2) gitlab (~> 4.19) http (~> 5.0) nokogiri (~> 1.10) @@ -354,7 +354,7 @@ DEPENDENCIES faraday-retry (~> 2.2) fog-core (= 2.1.0) fog-google (~> 1.19) - gitlab-qa (~> 13, >= 13.1.0) + gitlab-qa (~> 14) gitlab-utils! gitlab_quality-test_tooling (~> 1.11.0) influxdb-client (~> 3.0) @@ -380,4 +380,4 @@ DEPENDENCIES zeitwerk (~> 2.6, >= 2.6.12) BUNDLED WITH - 2.5.4 + 2.5.5 diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index c5ad7bca824..1d8a44de7d9 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -402,7 +402,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ it 'displays the remember me checkbox' do visit new_user_session_path - expect(page).to have_field('remember_me_omniauth') + expect(page).to have_field('js-remember-me-omniauth') end context 'when remember me is not enabled' do @@ -413,7 +413,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ it 'does not display the remember me checkbox' do visit new_user_session_path - expect(page).not_to have_field('remember_me_omniauth') + expect(page).not_to have_field('js-remember-me-omniauth') end end diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html deleted file mode 100644 index d7519dd695f..00000000000 --- a/spec/frontend/fixtures/static/oauth_remember_me.html +++ /dev/null @@ -1,21 +0,0 @@ -<div class="js-oauth-login"> - <input id="remember_me_omniauth" type="checkbox" /> - - <form method="post" action="http://example.com/"> - <button class="twitter" type="submit"> - <span>Twitter</span> - </button> - </form> - - <form method="post" action="http://example.com/"> - <button class="github" type="submit"> - <span>GitHub</span> - </button> - </form> - - <form method="post" action="http://example.com/?redirect_fragment=L1"> - <button class="facebook" type="submit"> - <span>Facebook</span> - </button> - </form> -</div> diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js deleted file mode 100644 index 4fea216302f..00000000000 --- a/spec/frontend/oauth_remember_me_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import $ from 'jquery'; -import htmlOauthRememberMe from 'test_fixtures_static/oauth_remember_me.html'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; - -describe('OAuthRememberMe', () => { - const findFormAction = (selector) => { - return $(`.js-oauth-login ${selector}`).parent('form').attr('action'); - }; - - beforeEach(() => { - setHTMLFixture(htmlOauthRememberMe); - - new OAuthRememberMe({ container: $('.js-oauth-login') }).bindEvents(); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => { - $('.js-oauth-login #remember_me_omniauth').click(); - - expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1'); - expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1'); - expect(findFormAction('.facebook')).toBe( - 'http://example.com/?redirect_fragment=L1&remember_me=1', - ); - - $('.js-oauth-login #remember_me_omniauth').click(); - - expect(findFormAction('.twitter')).toBe('http://example.com/'); - expect(findFormAction('.github')).toBe('http://example.com/'); - expect(findFormAction('.facebook')).toBe('http://example.com/?redirect_fragment=L1'); - }); -}); diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js index 7607381a981..60cf5dc65a2 100644 --- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js +++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js @@ -1,13 +1,12 @@ -import $ from 'jquery'; import htmlSessionsNew from 'test_fixtures/sessions/new.html'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment'; +import { + appendUrlFragment, + appendRedirectQuery, + toggleRememberMeQuery, +} from '~/pages/sessions/new/preserve_url_fragment'; describe('preserve_url_fragment', () => { - const findFormAction = (selector) => { - return $(`.js-oauth-login ${selector}`).parent('form').attr('action'); - }; - beforeEach(() => { setHTMLFixture(htmlSessionsNew); }); @@ -16,41 +15,74 @@ describe('preserve_url_fragment', () => { resetHTMLFixture(); }); - it('adds the url fragment to the login form actions', () => { - preserveUrlFragment('#L65'); + describe('non-OAuth login forms', () => { + describe('appendUrlFragment', () => { + const findFormAction = () => document.querySelector('.js-non-oauth-login form').action; - expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in#L65'); - }); + it('adds the url fragment to the login form actions', () => { + appendUrlFragment('#L65'); - it('does not add an empty url fragment to the login form actions', () => { - preserveUrlFragment(); + expect(findFormAction()).toBe('http://test.host/users/sign_in#L65'); + }); - expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in'); + it('does not add an empty url fragment to the login form actions', () => { + appendUrlFragment(); + + expect(findFormAction()).toBe('http://test.host/users/sign_in'); + }); + }); }); - it('does not add an empty query parameter to OmniAuth login buttons', () => { - preserveUrlFragment(); + describe('OAuth login forms', () => { + const findFormAction = (selector) => + document.querySelector(`.js-oauth-login #oauth-login-${selector}`).parentElement.action; - expect(findFormAction('#oauth-login-auth0')).toBe('http://test.host/users/auth/auth0'); - }); + describe('appendRedirectQuery', () => { + it('does not add an empty query parameter to the login form actions', () => { + appendRedirectQuery(); + + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0'); + }); + + describe('adds "redirect_fragment" query parameter to the login form actions', () => { + it('when "remember_me" is not present', () => { + appendRedirectQuery('#L65'); - describe('adds "redirect_fragment" query parameter to OmniAuth login buttons', () => { - it('when "remember_me" is not present', () => { - preserveUrlFragment('#L65'); + expect(findFormAction('auth0')).toBe( + 'http://test.host/users/auth/auth0?redirect_fragment=L65', + ); + }); - expect(findFormAction('#oauth-login-auth0')).toBe( - 'http://test.host/users/auth/auth0?redirect_fragment=L65', - ); + it('when "remember_me" is present', () => { + document + .querySelectorAll('form') + .forEach((form) => form.setAttribute('action', `${form.action}?remember_me=1`)); + + appendRedirectQuery('#L65'); + + expect(findFormAction('auth0')).toBe( + 'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65', + ); + }); + }); }); - it('when "remember-me" is present', () => { - $('.js-oauth-login form').attr('action', (i, href) => `${href}?remember_me=1`); + describe('toggleRememberMeQuery', () => { + const rememberMe = () => document.querySelector('#js-remember-me-omniauth'); + + it('toggles "remember_me" query parameter', () => { + toggleRememberMeQuery(); + + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0'); + + rememberMe().click(); + + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0?remember_me=1'); - preserveUrlFragment('#L65'); + rememberMe().click(); - expect(findFormAction('#oauth-login-auth0')).toBe( - 'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65', - ); + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0'); + }); }); }); }); diff --git a/spec/frontend/repository/components/commit_info_spec.js b/spec/frontend/repository/components/commit_info_spec.js index 4e570346d97..f868bc0623e 100644 --- a/spec/frontend/repository/components/commit_info_spec.js +++ b/spec/frontend/repository/components/commit_info_spec.js @@ -16,14 +16,15 @@ const commit = { const findTextExpander = () => wrapper.findComponent(GlButton); const findUserLink = () => wrapper.findByText(commit.author.name); +const findCommitterWrapper = () => wrapper.findByTestId('committer'); const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink); const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`); const findCommitRowDescription = () => wrapper.find('pre'); const findTitleHtml = () => wrapper.findByText(commit.titleHtml); -const createComponent = async ({ commitMock = {}, prevBlameLink } = {}) => { +const createComponent = async ({ commitMock = {}, prevBlameLink, span = 3 } = {}) => { wrapper = shallowMountExtended(CommitInfo, { - propsData: { commit: { ...commit, ...commitMock }, prevBlameLink }, + propsData: { commit: { ...commit, ...commitMock }, prevBlameLink, span }, }); await nextTick(); @@ -46,6 +47,22 @@ describe('Repository last commit component', () => { expect(findAuthorName().exists()).toBe(true); }); + it('truncates author name when commit spans less than 3 lines', () => { + createComponent({ span: 2 }); + + expect(findCommitterWrapper().classes()).toEqual([ + 'committer', + 'gl-flex-basis-full', + 'gl-display-inline-flex', + ]); + expect(findUserLink().classes()).toEqual([ + 'commit-author-link', + 'js-user-link', + 'gl-display-inline-block', + 'gl-text-truncate', + ]); + }); + it('does not render description expander when description is null', () => { createComponent(); diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js index 295b08f4b1c..194d33ae6b9 100644 --- a/spec/frontend/tracking/internal_events_spec.js +++ b/spec/frontend/tracking/internal_events_spec.js @@ -4,6 +4,7 @@ import InternalEvents from '~/tracking/internal_events'; import { LOAD_INTERNAL_EVENTS_SELECTOR } from '~/tracking/constants'; import * as utils from '~/tracking/utils'; import { Tracker } from '~/tracking/tracker'; +import Tracking from '~/tracking'; jest.mock('~/api', () => ({ trackInternalEvent: jest.fn(), @@ -20,13 +21,23 @@ const event = 'TestEvent'; describe('InternalEvents', () => { describe('trackEvent', () => { + const category = 'TestCategory'; + it('trackEvent calls API.trackInternalEvent with correct arguments', () => { - InternalEvents.trackEvent(event); + InternalEvents.trackEvent(event, category); expect(API.trackInternalEvent).toHaveBeenCalledTimes(1); expect(API.trackInternalEvent).toHaveBeenCalledWith(event); }); + it('trackEvent calls Tracking.event with correct arguments including category', () => { + jest.spyOn(Tracking, 'event').mockImplementation(() => {}); + + InternalEvents.trackEvent(event, category); + + expect(Tracking.event).toHaveBeenCalledWith(category, event, expect.any(Object)); + }); + it('trackEvent calls trackBrowserSDK with correct arguments', () => { jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {}); @@ -63,7 +74,7 @@ describe('InternalEvents', () => { await wrapper.findByTestId('button').trigger('click'); expect(trackEventSpy).toHaveBeenCalledTimes(1); - expect(trackEventSpy).toHaveBeenCalledWith(event); + expect(trackEventSpy).toHaveBeenCalledWith(event, undefined); }); }); diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index f6e178f5b28..96201f0827f 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -42,7 +42,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages incident_management_timeline_event_tags visible_forks inherited_ci_variables autocomplete_users ci_cd_settings detailed_import_status value_streams ml_models - allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers + allows_multiple_merge_request_assignees allows_multiple_merge_request_reviewers is_forked ] expect(described_class).to include_graphql_fields(*expected_fields) @@ -771,6 +771,57 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj end end + describe 'is_forked' do + let_it_be(:user) { create(:user) } + let_it_be(:unforked_project) { create(:project, :public) } + let!(:forked_project) { fork_project(unforked_project) } + let(:project) { nil } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + isForked + } + } + ) + end + + let(:response) { GitlabSchema.execute(query).as_json } + + subject(:is_forked) { response.dig('data', 'project', 'isForked') } + + context 'when project has a fork network' do + context 'when fork is itself' do + let(:project) { unforked_project } + + it { is_expected.to be false } + end + + context 'when fork is not itself' do + let(:project) { forked_project } + + it { is_expected.to be true } + + it 'avoids N+1 queries' do + query_count = ActiveRecord::QueryRecorder.new { response } + + expect(query_count).not_to exceed_query_limit(8) + end + end + end + + context 'when project does not have a fork network' do + let(:project) { unforked_project } + + before do + allow(project).to receive(:fork_network).and_return(nil) + end + + it { is_expected.to be false } + end + end + describe 'branch_rules' do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) } diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index d1dee70e34d..6a2449cbcdb 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -860,13 +860,30 @@ RSpec.describe API::Helpers, feature_category: :shared do ) end - it 'logs an exception for unknown event' do + it 'tracks an exception and renders 422 for unknown event', :aggregate_failures do expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(Gitlab::InternalEvents::UnknownEventError, "Unknown event: #{unknown_event}") - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + + expect(Gitlab::ErrorTracking).to receive(:track_exception) .with( instance_of(Gitlab::InternalEvents::UnknownEventError), event_name: unknown_event ) + expect(helper).to receive(:unprocessable_entity!).with("Unknown event: #{unknown_event}") + + helper.track_event(unknown_event, + user: user, + namespace_id: namespace.id, + project_id: project.id + ) + end + + it 'logs an exception for tracking errors' do + expect(Gitlab::InternalEvents).to receive(:track_event).and_raise(ArgumentError, "Error message") + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + .with( + instance_of(ArgumentError), + event_name: unknown_event + ) helper.track_event(unknown_event, user: user, diff --git a/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb index 65d4e8b4978..5fc0c8fa239 100644 --- a/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb +++ b/spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb @@ -87,6 +87,12 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category expect(member.expires_at).to eq(nil) end + it 'does not send new member notification' do + expect(NotificationService).not_to receive(:new) + + subject.load(context, member_data) + end + context 'when user_id is current user id' do it 'does not create new membership' do data = { user_id: user.id } diff --git a/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb b/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb index fad10aba882..dc62a520d07 100644 --- a/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb +++ b/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus do +RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLockedStatus, feature_category: :build_artifacts do describe '#perform' do let(:batch_table) { :ci_pipeline_artifacts } let(:batch_column) { :id } @@ -30,11 +30,11 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLock let(:locked_pipeline) { pipelines.create!(locked: locked, partition_id: 100) } # rubocop:disable Layout/LineLength - let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown) } - let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown) } - let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown) } - let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked) } - let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked) } + let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown, partition_id: 100) } + let!(:unlocked_artifact_1) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 2048, file_type: 1, file_format: 'raw', file: 'b', locked: unknown, partition_id: 100) } + let!(:unlocked_artifact_2) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 4096, file_type: 2, file_format: 'gzip', file: 'c.gz', locked: unknown, partition_id: 100) } + let!(:already_unlocked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: unlocked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: unlocked, partition_id: 100) } + let!(:already_locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 8192, file_type: 3, file_format: 'raw', file: 'd', locked: locked, partition_id: 100) } # rubocop:enable Layout/LineLength subject do diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb index b34eb7964ca..edefddfc9d7 100644 --- a/spec/models/fork_network_member_spec.rb +++ b/spec/models/fork_network_member_spec.rb @@ -25,4 +25,30 @@ RSpec.describe ForkNetworkMember do expect(ForkNetwork.count).to eq(1) end end + + describe '#by_projects' do + let_it_be(:fork_network_member_1) { create(:fork_network_member) } + let_it_be(:fork_network_member_2) { create(:fork_network_member) } + + it 'returns fork network members by project ids' do + expect( + described_class.by_projects( + [fork_network_member_1.project_id, fork_network_member_2.project_id] + ) + ).to match_array([fork_network_member_1, fork_network_member_2]) + end + end + + describe '#with_fork_network' do + let_it_be(:fork_network_member_1) { create(:fork_network_member) } + let_it_be(:fork_network_member_2) { create(:fork_network_member) } + + it 'avoids N+1 queries' do + query_count = ActiveRecord::QueryRecorder.new do + described_class.all.with_fork_network.find_each(&:fork_network) + end + + expect(query_count).not_to exceed_query_limit(1) + end + end end diff --git a/spec/requests/groups/autocomplete_sources_spec.rb b/spec/requests/groups/autocomplete_sources_spec.rb index 02fb04a4af8..5d190074534 100644 --- a/spec/requests/groups/autocomplete_sources_spec.rb +++ b/spec/requests/groups/autocomplete_sources_spec.rb @@ -14,6 +14,42 @@ RSpec.describe 'groups autocomplete', feature_category: :groups_and_projects do sign_in(user) end + describe '#members' do + context 'when type is WorkItem' do + let(:type) { 'Workitem' } + + it 'returns the correct response', :aggregate_failures do + work_item = create(:work_item, :group_level, namespace: group, author: user) + + get members_group_autocomplete_sources_path(group, type_id: work_item.iid, type: type) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response).to contain_exactly( + hash_including('type' => 'User', 'username' => user.username), + hash_including('type' => 'Group', 'username' => group.full_path) + ) + end + end + + context 'when type is Issue' do + let(:type) { 'Issue' } + + it 'returns the correct response', :aggregate_failures do + issue = create(:issue, :group_level, namespace: group, author: user) + + get members_group_autocomplete_sources_path(group, type_id: issue.iid, type: type) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response).to contain_exactly( + hash_including('type' => 'User', 'username' => user.username), + hash_including('type' => 'Group', 'username' => group.full_path) + ) + end + end + end + describe '#issues' do using RSpec::Parameterized::TableSyntax diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb index 5f4e92cf955..311f2680379 100644 --- a/spec/services/quick_actions/target_service_spec.rb +++ b/spec/services/quick_actions/target_service_spec.rb @@ -3,13 +3,11 @@ require 'spec_helper' RSpec.describe QuickActions::TargetService, feature_category: :team_planning do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:service) { described_class.new(project, user) } - - before do - project.add_maintainer(user) - end + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be(:user) { create(:user).tap { |u| project.add_maintainer(u) } } + let(:container) { project } + let(:service) { described_class.new(container: container, current_user: user) } describe '#execute' do shared_examples 'no target' do |type_iid:| @@ -32,7 +30,7 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do it 'builds a new target' do target = service.execute(type, type_iid) - expect(target.project).to eq(project) + expect(target.resource_parent).to eq(container) expect(target).to be_new_record end end @@ -45,6 +43,15 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do it_behaves_like 'find target' it_behaves_like 'build target', type_iid: nil it_behaves_like 'build target', type_iid: -1 + + context 'when issue belongs to a group' do + let(:container) { group } + let(:target) { create(:issue, :group_level, namespace: group) } + + it_behaves_like 'find target' + it_behaves_like 'build target', type_iid: nil + it_behaves_like 'build target', type_iid: -1 + end end context 'for work item' do @@ -53,6 +60,13 @@ RSpec.describe QuickActions::TargetService, feature_category: :team_planning do let(:type) { 'WorkItem' } it_behaves_like 'find target' + + context 'when work item belongs to a group' do + let(:container) { group } + let(:target) { create(:work_item, :group_level, namespace: group) } + + it_behaves_like 'find target' + end end context 'for merge request' do diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index cc45cb1292d..7752488ab44 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -112,7 +112,7 @@ module LoginHelpers visit new_user_session_path expect(page).to have_css('.js-oauth-login') - check 'remember_me_omniauth' if remember_me + check 'js-remember-me-omniauth' if remember_me click_button "oauth-login-#{provider}" end |