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(/^#/, ''); +export function appendUrlFragment(fragment = document.location.hash) { + if (!fragment) { + return; + } - // 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); - }); + 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" /> -
+
{{ commit.author.name }} - {{ $options.i18n.authored }} + {{ $options.i18n.authored }} 
     
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. | `importStatus` | [`String`](#string) | Status of import background job of the project. | | `incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. | | `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. | +| `isForked` | [`Boolean!`](#boolean) | Project is forked. | | `issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. | | `issuesEnabled` | [`Boolean`](#boolean) | Indicates if Issues are enabled for the current user. | | `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: - `` with your trigger token. - `` with a branch or tag name, like `main`. - `` 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. - `` 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). - `` 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 | -Footnotes: + Footnotes:
    -
  1. 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.
  2. +
  3. The SpotBugs-based analyzer supports Gradle, Maven, and SBT. It can also be used with variants like the Gradle wrapper, Grails, and the Maven wrapper. However, SpotBugs has limitations when used against Ant-based projects. You should use the Semgrep-based analyzer for Ant-based Java or Scala projects.
-
## 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 @@ - 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'); + + expect(findFormAction()).toBe('http://test.host/users/sign_in#L65'); + }); + + 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 url fragment to the login form actions', () => { - preserveUrlFragment(); + describe('OAuth login forms', () => { + const findFormAction = (selector) => + document.querySelector(`.js-oauth-login #oauth-login-${selector}`).parentElement.action; - expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in'); - }); + describe('appendRedirectQuery', () => { + it('does not add an empty query parameter to the login form actions', () => { + appendRedirectQuery(); - it('does not add an empty query parameter to OmniAuth login buttons', () => { - preserveUrlFragment(); + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0'); + }); - expect(findFormAction('#oauth-login-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'); - preserveUrlFragment('#L65'); + it('toggles "remember_me" query parameter', () => { + toggleRememberMeQuery(); - 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'); + + rememberMe().click(); + + expect(findFormAction('auth0')).toBe('http://test.host/users/auth/auth0?remember_me=1'); + + rememberMe().click(); + + 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