diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ad92618e1b4..398ff145a66 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -c8a29dc9fd507cab8835b2e1152b94a6ac96de35 +a8d42fb628ab8d4cbde0481d25724963f73e7931 diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index a48405ae3e9..dde5ea81e9a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -20,7 +20,7 @@ import { } from '../constants'; import eventHub from '../event_hub'; import { DIFF_FILE, GENERIC_ERROR } from '../i18n'; -import { collapsedType, isCollapsed, getShortShaFromFile } from '../utils/diff_file'; +import { collapsedType, getShortShaFromFile } from '../utils/diff_file'; import DiffContent from './diff_content.vue'; import DiffFileHeader from './diff_file_header.vue'; @@ -84,7 +84,7 @@ export default { return { isLoadingCollapsedDiff: false, forkMessageVisible: false, - isCollapsed: isCollapsed(this.file), + hasToggled: false, }; }, i18n: { @@ -102,9 +102,7 @@ export default { return getShortShaFromFile(this.file); }, showLoadingIcon() { - return ( - this.idState.isLoadingCollapsedDiff || (!this.file.renderIt && !this.idState.isCollapsed) - ); + return this.idState.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); }, hasDiff() { return hasDiff(this.file); @@ -148,13 +146,13 @@ export default { return collapsedType(this.file) === DIFF_FILE_MANUAL_COLLAPSE; }, showBody() { - return !this.idState.isCollapsed || this.automaticallyCollapsed; + return !this.isCollapsed || this.automaticallyCollapsed; }, showWarning() { - return this.idState.isCollapsed && this.automaticallyCollapsed && !this.viewDiffsFileByFile; + return this.isCollapsed && this.automaticallyCollapsed && !this.viewDiffsFileByFile; }, showContent() { - return !this.idState.isCollapsed && !this.isFileTooLarge; + return !this.isCollapsed && !this.isFileTooLarge; }, showLocalFileReviews() { const loggedIn = Boolean(gon.current_user_id); @@ -165,6 +163,13 @@ export default { codequalityDiffForFile() { return this.codequalityDiff?.files?.[this.file.file_path] || []; }, + isCollapsed() { + if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) { + return this.viewDiffsFileByFile ? false : this.file.viewer?.automaticallyCollapsed; + } + + return this.file.viewer?.manuallyCollapsed; + }, }, watch: { 'file.id': { @@ -176,23 +181,11 @@ export default { }, 'file.file_hash': { handler: function hashChangeWatch(newHash, oldHash) { - this.isCollapsed = isCollapsed(this.file); - if (newHash && oldHash && !this.hasDiff && !this.preRender) { this.requestDiff(); } }, }, - 'file.viewer.automaticallyCollapsed': { - handler: function autoChangeWatch(automaticValue) { - this.handleAutomaticallyCollapsed(automaticValue); - }, - }, - 'file.viewer.manuallyCollapsed': { - handler: function manualChangeWatch(manualValue) { - this.handleManualChangeWatch(manualValue); - }, - }, }, created() { if (this.preRender) return; @@ -208,8 +201,6 @@ export default { } this.manageViewedEffects(); - this.handleAutomaticallyCollapsed(this.file.viewer.automaticallyCollapsed); - this.handleManualChangeWatch(this.file.viewer.manuallyCollapsed); }, beforeDestroy() { if (this.preRender) return; @@ -224,12 +215,18 @@ export default { 'setFileCollapsedByUser', ]), manageViewedEffects() { - if (this.reviewed && !this.idState.isCollapsed && this.showLocalFileReviews) { + if ( + !this.idState.hasToggled && + this.reviewed && + !this.isCollapsed && + this.showLocalFileReviews + ) { this.handleToggle(); + this.idState.hasToggled = true; } }, expandAllListener() { - if (this.idState.isCollapsed) { + if (this.isCollapsed) { this.handleToggle(); } }, @@ -251,7 +248,7 @@ export default { }); }, handleToggle({ viaUserInteraction = false } = {}) { - const collapsingNow = !this.idState.isCollapsed; + const collapsingNow = !this.isCollapsed; const contentElement = this.$el.querySelector(`#diff-content-${this.file.file_hash}`); this.setFileCollapsedByUser({ @@ -297,16 +294,6 @@ export default { hideForkMessage() { this.idState.forkMessageVisible = false; }, - handleAutomaticallyCollapsed(automaticValue) { - if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) { - this.idState.isCollapsed = this.viewDiffsFileByFile ? false : automaticValue; - } - }, - handleManualChangeWatch(manualValue) { - if (manualValue !== null) { - this.idState.isCollapsed = manualValue; - } - }, }, }; @@ -328,7 +315,7 @@ export default { :diff-file="file" :collapsible="true" :reviewed="reviewed" - :expanded="!idState.isCollapsed" + :expanded="!isCollapsed" :add-merge-request-buttons="true" :view-diffs-file-by-file="viewDiffsFileByFile" :show-local-file-reviews="showLocalFileReviews" diff --git a/app/controllers/users/unsubscribes_controller.rb b/app/controllers/users/unsubscribes_controller.rb new file mode 100644 index 00000000000..9ac07083cd5 --- /dev/null +++ b/app/controllers/users/unsubscribes_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Users + class UnsubscribesController < ApplicationController + skip_before_action :authenticate_user! + + feature_category :users + + def show + @user = get_user + end + + def create + @user = get_user + + if @user + @user.admin_unsubscribe! + Notify.send_unsubscribed_notification(@user.id).deliver_later + end + + redirect_to new_user_session_path, notice: 'You have been unsubscribed' + end + + protected + + # rubocop: disable CodeReuse/ActiveRecord + def get_user + @email = Base64.urlsafe_decode64(params[:email]) + User.find_by(email: @email) + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 47caf83eb1c..ce2f184bcb4 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -65,7 +65,7 @@ module Types end def visible?(context) - return false if feature_flag.present? && !Feature.enabled?(feature_flag) + return false if feature_flag.present? && !Feature.enabled?(feature_flag, default_enabled: :yaml) super end diff --git a/app/mailers/emails/admin_notification.rb b/app/mailers/emails/admin_notification.rb new file mode 100644 index 00000000000..f4540ef81a5 --- /dev/null +++ b/app/mailers/emails/admin_notification.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Emails + module AdminNotification + def send_admin_notification(user_id, subject, body) + user = User.find(user_id) + email = user.notification_email + @unsubscribe_url = unsubscribe_url(email: Base64.urlsafe_encode64(email)) + @body = body + mail to: email, subject: subject + end + + def send_unsubscribed_notification(user_id) + user = User.find(user_id) + email = user.notification_email + mail to: email, subject: "Unsubscribed from GitLab administrator notifications" + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index dd75ab4bf03..03b70fffde1 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -22,6 +22,7 @@ class Notify < ApplicationMailer include Emails::Reviews include Emails::ServiceDesk include Emails::InProductMarketing + include Emails::AdminNotification helper TimeboxesHelper helper MergeRequestsHelper diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 00a39728436..83ede2867cc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -193,8 +193,7 @@ module Ci acts_as_taggable - add_authentication_token_field :token, - encrypted: -> { Gitlab::Ci::Features.require_builds_token_encryption? ? :required : :optional } + add_authentication_token_field :token, encrypted: :required before_save :ensure_token before_destroy { unscoped_project } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3a6bebb610b..6b8c5d21345 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1026,8 +1026,6 @@ module Ci end def can_generate_codequality_reports? - return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project) - has_reports?(Ci::JobArtifact.codequality_reports) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 06512d2fdb9..7ca83d1d68c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1556,8 +1556,6 @@ class MergeRequest < ApplicationRecord end def has_codequality_mr_diff_report? - return false unless ::Gitlab::Ci::Features.display_quality_on_mr_diff?(project) - actual_head_pipeline&.has_codequality_mr_diff_report? end diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb index 90bd9b312c1..d9e8743aa50 100644 --- a/app/models/namespaces/traversal/recursive.rb +++ b/app/models/namespaces/traversal/recursive.rb @@ -78,7 +78,7 @@ module Namespaces alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids def object_hierarchy(ancestors_base) - Gitlab::ObjectHierarchy.new(ancestors_base, options: { use_distinct: Feature.enabled?(:use_distinct_in_object_hierarchy, self) }) + Gitlab::ObjectHierarchy.new(ancestors_base) end end end diff --git a/app/models/user.rb b/app/models/user.rb index eb93d717e9b..6b7bf7fa822 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1305,6 +1305,10 @@ class User < ApplicationRecord save if notification_email_changed? || public_email_changed? || commit_email_changed? end + def admin_unsubscribe! + update_column :admin_email_unsubscribed_at, Time.current + end + def set_projects_limit # `User.select(:id)` raises # `ActiveModel::MissingAttributeError: missing attribute: projects_limit` diff --git a/app/services/ci/queue/build_queue_service.rb b/app/services/ci/queue/build_queue_service.rb index 8190599fbb5..99408d529b2 100644 --- a/app/services/ci/queue/build_queue_service.rb +++ b/app/services/ci/queue/build_queue_service.rb @@ -28,7 +28,7 @@ module Ci groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces) hierarchy_groups = Gitlab::ObjectHierarchy - .new(groups, options: { use_distinct: ::Feature.enabled?(:use_distinct_in_register_job_object_hierarchy) }) + .new(groups) .base_and_descendants projects = Project.where(namespace_id: hierarchy_groups) diff --git a/app/views/notify/send_admin_notification.html.haml b/app/views/notify/send_admin_notification.html.haml new file mode 100644 index 00000000000..f7f1528f332 --- /dev/null +++ b/app/views/notify/send_admin_notification.html.haml @@ -0,0 +1,7 @@ += simple_format @body + +\---- + +%p + Don't want to receive updates from GitLab administrators? + = link_to 'Unsubscribe', @unsubscribe_url diff --git a/app/views/notify/send_admin_notification.text.haml b/app/views/notify/send_admin_notification.text.haml new file mode 100644 index 00000000000..bfacbd3dcb2 --- /dev/null +++ b/app/views/notify/send_admin_notification.text.haml @@ -0,0 +1,6 @@ += h @body + +\----- + +Don't want to receive updates from GitLab administrators? +Unsubscribe here: #{@unsubscribe_url} diff --git a/app/views/notify/send_unsubscribed_notification.html.haml b/app/views/notify/send_unsubscribed_notification.html.haml new file mode 100644 index 00000000000..9f68feeaa31 --- /dev/null +++ b/app/views/notify/send_unsubscribed_notification.html.haml @@ -0,0 +1,2 @@ +%p + You have been unsubscribed from receiving GitLab administrator notifications. diff --git a/app/views/notify/send_unsubscribed_notification.text.haml b/app/views/notify/send_unsubscribed_notification.text.haml new file mode 100644 index 00000000000..5edc1ddcdae --- /dev/null +++ b/app/views/notify/send_unsubscribed_notification.text.haml @@ -0,0 +1 @@ +You have been unsubscribed from receiving GitLab administrator notifications. diff --git a/app/views/users/unsubscribes/show.html.haml b/app/views/users/unsubscribes/show.html.haml new file mode 100644 index 00000000000..8b3dc69f3a7 --- /dev/null +++ b/app/views/users/unsubscribes/show.html.haml @@ -0,0 +1,11 @@ +- page_title _("Unsubscribe"), _("Admin Notifications") +%h3.page-title Unsubscribe from Admin notifications + +%hr += form_tag unsubscribe_path(Base64.urlsafe_encode64(@email)) do + %p + Yes, I want to unsubscribe + %strong= @email + from any further admin emails. + .form-actions + = submit_tag 'Unsubscribe', class: 'gl-button btn btn-confirm' diff --git a/config/feature_flags/development/ci_builds_tokens_required_encryption.yml b/config/feature_flags/development/ci_builds_tokens_required_encryption.yml deleted file mode 100644 index de5a9e54e6e..00000000000 --- a/config/feature_flags/development/ci_builds_tokens_required_encryption.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_builds_tokens_required_encryption -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63874 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333566 -milestone: '14.0' -type: development -group: group::pipeline execution -default_enabled: false diff --git a/config/feature_flags/development/codequality_mr_diff.yml b/config/feature_flags/development/codequality_mr_diff.yml deleted file mode 100644 index ca6846b9390..00000000000 --- a/config/feature_flags/development/codequality_mr_diff.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: codequality_mr_diff -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47938 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284140 -milestone: '13.7' -type: development -group: group::testing -default_enabled: false diff --git a/config/feature_flags/development/use_distinct_for_all_object_hierarchy.yml b/config/feature_flags/development/use_distinct_for_all_object_hierarchy.yml deleted file mode 100644 index 9412e6af327..00000000000 --- a/config/feature_flags/development/use_distinct_for_all_object_hierarchy.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_distinct_for_all_object_hierarchy -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57052 -rollout_issue_url: -milestone: '13.11' -type: development -group: group::database -default_enabled: false diff --git a/config/feature_flags/development/use_distinct_in_object_hierarchy.yml b/config/feature_flags/development/use_distinct_in_object_hierarchy.yml deleted file mode 100644 index 7604606ea88..00000000000 --- a/config/feature_flags/development/use_distinct_in_object_hierarchy.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_distinct_in_object_hierarchy -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56509 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324644 -milestone: '13.10' -type: development -group: group::optimize -default_enabled: false diff --git a/config/feature_flags/development/use_distinct_in_register_job_object_hierarchy.yml b/config/feature_flags/development/use_distinct_in_register_job_object_hierarchy.yml deleted file mode 100644 index b9652d26834..00000000000 --- a/config/feature_flags/development/use_distinct_in_register_job_object_hierarchy.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_distinct_in_register_job_object_hierarchy -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57045 -rollout_issue_url: -milestone: '13.11' -type: development -group: group::pipeline execution -default_enabled: false diff --git a/config/routes/user.rb b/config/routes/user.rb index 5f746eb6670..109179f76f1 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +get 'unsubscribes/:email', to: 'users/unsubscribes#show', as: :unsubscribe +post 'unsubscribes/:email', to: 'users/unsubscribes#create' + # Allows individual providers to be directed to a chosen controller # Call from inside devise_scope def override_omniauth(provider, controller, path_prefix = '/users/auth') diff --git a/db/post_migrate/20210604133651_schedule_merge_request_diff_users_background_migration.rb b/db/post_migrate/20210604133651_schedule_merge_request_diff_users_background_migration.rb index e0af0ef1fc6..b9b694012f2 100644 --- a/db/post_migrate/20210604133651_schedule_merge_request_diff_users_background_migration.rb +++ b/db/post_migrate/20210604133651_schedule_merge_request_diff_users_background_migration.rb @@ -1,51 +1,8 @@ # frozen_string_literal: true class ScheduleMergeRequestDiffUsersBackgroundMigration < ActiveRecord::Migration[6.1] - include Gitlab::Database::MigrationHelpers - - disable_ddl_transaction! - - # The number of rows to process in a single migration job. - # - # The minimum interval for background migrations is two minutes. On staging we - # observed we can process roughly 20 000 rows in a minute. Based on the total - # number of rows on staging, this translates to a total processing time of - # roughly 14 days. - # - # By using a batch size of 40 000, we maintain a rate of roughly 20 000 rows - # per minute, hopefully keeping the total migration time under two weeks; - # instead of four weeks. - BATCH_SIZE = 40_000 - - MIGRATION_NAME = 'MigrateMergeRequestDiffCommitUsers' - - class MergeRequestDiff < ActiveRecord::Base - self.table_name = 'merge_request_diffs' - end - def up - start = MergeRequestDiff.minimum(:id).to_i - max = MergeRequestDiff.maximum(:id).to_i - delay = BackgroundMigrationWorker.minimum_interval - - # The table merge_request_diff_commits contains _a lot_ of rows (roughly 400 - # 000 000 on staging). Iterating a table that large to determine job ranges - # would take a while. - # - # To avoid that overhead, we simply schedule fixed ranges according to the - # minimum and maximum IDs. The background migration in turn only processes - # rows that actually exist. - while start < max - stop = start + BATCH_SIZE - - migrate_in(delay, MIGRATION_NAME, [start, stop]) - - Gitlab::Database::BackgroundMigrationJob - .create!(class_name: MIGRATION_NAME, arguments: [start, stop]) - - delay += BackgroundMigrationWorker.minimum_interval - start += BATCH_SIZE - end + # no-op end def down diff --git a/db/post_migrate/20210628124505_reset_job_token_scope_enabled.rb b/db/post_migrate/20210628124505_reset_job_token_scope_enabled.rb new file mode 100644 index 00000000000..1176e704d0a --- /dev/null +++ b/db/post_migrate/20210628124505_reset_job_token_scope_enabled.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ResetJobTokenScopeEnabled < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + def up + with_lock_retries do + remove_column :project_ci_cd_settings, :job_token_scope_enabled + add_column :project_ci_cd_settings, :job_token_scope_enabled, :boolean, default: false, null: false + end + end + + def down + # Irreversible + end +end diff --git a/db/post_migrate/20210708130419_reschedule_merge_request_diff_users_background_migration.rb b/db/post_migrate/20210708130419_reschedule_merge_request_diff_users_background_migration.rb new file mode 100644 index 00000000000..53f13ca96d2 --- /dev/null +++ b/db/post_migrate/20210708130419_reschedule_merge_request_diff_users_background_migration.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class RescheduleMergeRequestDiffUsersBackgroundMigration < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + # The number of rows to process in a single migration job. + # + # The minimum interval for background migrations is two minutes. On staging we + # observed we can process roughly 20 000 rows in a minute. Based on the total + # number of rows on staging, this translates to a total processing time of + # roughly 14 days. + # + # By using a batch size of 40 000, we maintain a rate of roughly 20 000 rows + # per minute, hopefully keeping the total migration time under two weeks; + # instead of four weeks. + BATCH_SIZE = 40_000 + + MIGRATION_NAME = 'MigrateMergeRequestDiffCommitUsers' + + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + end + + def up + start = MergeRequestDiff.minimum(:id).to_i + max = MergeRequestDiff.maximum(:id).to_i + delay = BackgroundMigrationWorker.minimum_interval + + Gitlab::Database::BackgroundMigrationJob + .where(class_name: MIGRATION_NAME) + .delete_all + + # The table merge_request_diff_commits contains _a lot_ of rows (roughly 400 + # 000 000 on staging). Iterating a table that large to determine job ranges + # would take a while. + # + # To avoid that overhead, we simply schedule fixed ranges according to the + # minimum and maximum IDs. The background migration in turn only processes + # rows that actually exist. + while start < max + stop = start + BATCH_SIZE + + migrate_in(delay, MIGRATION_NAME, [start, stop]) + + Gitlab::Database::BackgroundMigrationJob + .create!(class_name: MIGRATION_NAME, arguments: [start, stop]) + + delay += BackgroundMigrationWorker.minimum_interval + start += BATCH_SIZE + end + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20210628124505 b/db/schema_migrations/20210628124505 new file mode 100644 index 00000000000..478617f5506 --- /dev/null +++ b/db/schema_migrations/20210628124505 @@ -0,0 +1 @@ +7add197fec50d8da5bcdbca83115558480668c26ad3a3fefc4ab93c07f34f63a \ No newline at end of file diff --git a/db/schema_migrations/20210708130419 b/db/schema_migrations/20210708130419 new file mode 100644 index 00000000000..b20db5b17c2 --- /dev/null +++ b/db/schema_migrations/20210708130419 @@ -0,0 +1 @@ +8545d6575c9dacec6796882677c4403cf3559430518e8709bf390f20717413d7 \ No newline at end of file diff --git a/doc/api/markdown.md b/doc/api/markdown.md index 51dc8a22bf1..d83b7420829 100644 --- a/doc/api/markdown.md +++ b/doc/api/markdown.md @@ -14,7 +14,7 @@ Available only in APIv4. ## Render an arbitrary Markdown document ```plaintext -POST /api/v4/markdown +POST /markdown ``` | Attribute | Type | Required | Description | diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index a080d513afa..fcee3f7a637 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -4,21 +4,24 @@ group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# GitLab.com for Jira Cloud app **(FREE SAAS)** +# GitLab.com for Jira Cloud app **(FREE)** + +## GitLab.com for Jira Cloud app **(FREE SAAS)** You can integrate GitLab.com and Jira Cloud using the [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) -app in the Atlassian Marketplace. The user configuring GitLab.com for Jira Cloud must have +app in the Atlassian Marketplace. The user configuring GitLab.com for Jira Cloud app must have [Maintainer](../../user/permissions.md) permissions in the GitLab.com namespace. This integration method supports [smart commits](dvcs.md#smart-commits). This method is recommended when using GitLab.com and Jira Cloud because data is synchronized in real-time. The DVCS connector updates data only once per hour. -If you are not using both of these environments, use the [Jira DVCS Connector](dvcs.md) method. +If you are not using both of these environments, use the [Jira DVCS Connector](dvcs.md) method or +[steps to install GitLab.com for Jira Cloud app for self-managed instances](#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances). -For a walkthrough of the integration with GitLab.com for Jira Cloud, watch +For a walkthrough of the integration with GitLab.com for Jira Cloud app, watch [Configure GitLab.com Jira Could Integration using Marketplace App](https://youtu.be/SwR-g1s1zTo) on YouTube. 1. Go to **Jira Settings > Apps > Find new apps**, then search for GitLab. @@ -52,10 +55,10 @@ After a namespace is added: Support for syncing past branch and commit data [is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/263240). -## Install the GitLab.com for Jira Cloud application for self-managed instances **(FREE SELF)** +## Install the GitLab.com for Jira Cloud app for self-managed instances **(FREE SELF)** If your GitLab instance is self-managed, you must follow some -extra steps to install the GitLab.com for Jira Cloud application. +extra steps to install the GitLab.com for Jira Cloud app. Each Jira Cloud application must be installed from a single location. Jira fetches a [manifest file](https://developer.atlassian.com/cloud/jira/platform/connect-app-descriptor/) @@ -121,9 +124,9 @@ for details. NOTE: DVCS means distributed version control system. -## Troubleshooting GitLab.com for Jira Cloud +## Troubleshooting GitLab.com for Jira Cloud app -The GitLab.com for Jira Cloud app uses an iframe to add namespaces on the settings page. Some browsers block cross-site cookies. This can lead to a message saying that the user needs to log in on GitLab.com even though the user is already logged in. +The GitLab.com for Jira Cloud app uses an iframe to add namespaces on the settings page. Some browsers block cross-site cookies, which can lead to a message saying that the user needs to log in on GitLab.com even though the user is already logged in. > "You need to sign in or sign up before continuing." diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 3aba2e3b3a0..9057267d55e 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -31,11 +31,6 @@ This integration connects all GitLab projects to projects in the Jira instance i including the projects in its subgroups. - A personal namespace: Connects the projects in that personal namespace to Jira. -This differs from the [Jira integration](index.md), -where the mapping is between one GitLab project and the entire Jira instance. -You can install both integrations to take advantage of both sets of features. -A [feature comparison](index.md#direct-feature-comparison) is available. - ## Use the integration After the integration is [set up on GitLab and Jira](#configure-the-integration), you can: @@ -44,7 +39,8 @@ After the integration is [set up on GitLab and Jira](#configure-the-integration) commit messages, and merge request titles. - See the linked branches, commits, and merge requests in Jira issues: -Merge requests are called "pull requests" in Jira issues. +At this time, merge requests are called "pull requests" in Jira issues. +This name may change in a future Jira release. Select the links to see your GitLab repository data. @@ -72,13 +68,13 @@ To simplify administration, we recommend that a GitLab group maintainer or group | Jira usage | GitLab.com customers need | GitLab self-managed customers need | |------------|---------------------------|------------------------------------| -| [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-application-for-self-managed-instances) for more information. | +| [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances) for more information. | | Your own server | The Jira DVCS (distributed version control system) connector. This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). | Each GitLab project can be configured to connect to an entire Jira instance. That means after configuration, one GitLab project can interact with all Jira projects in that instance. For: -- The [view Jira issues](issues.md#view-jira-issues) feature, you must associate a GitLab project with a +- The [view Jira issues](issues.md#view-jira-issues) feature **(PREMIUM)**, you must associate a GitLab project with a specific Jira project. - Other features, you do not have to explicitly associate a GitLab project with any single Jira project. @@ -86,16 +82,16 @@ configuration, one GitLab project can interact with all Jira projects in that in If you have a single Jira instance, you can pre-fill the settings. For more information, read the documentation for [central administration of project integrations](../../user/admin_area/settings/project_integration_management.md). -To enable the Jira service in GitLab, you must: +To enable the integration in GitLab, you must: -1. [Configure the project in Jira](dvcs.md#configure-jira-for-dvcs). +1. [Configure the project in Jira](index.md#jira-integration). The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`. 1. [Enter the correct values in GitLab](#configure-gitlab). ### Configure GitLab To enable the integration in your GitLab project, after you -[configure your Jira project](dvcs.md#configure-jira-for-dvcs): +[configure your Jira project](index.md#jira-integration): 1. Ensure your GitLab installation does not use a relative URL, as described in [Limitations](#limitations). @@ -114,12 +110,14 @@ To enable the integration in your GitLab project, after you this GitLab project, such as `https://jira.example.com`. - **Jira API URL**: The base URL to the Jira instance API, such as `https://jira-api.example.com`. Defaults to the **Web URL** value if not set. Leave blank if using **Jira on Atlassian cloud**. - - **Username or Email**: Created when you [configured Jira](dvcs.md#configure-jira-for-dvcs). + - **Username or Email**: For **Jira Server**, use `username`. For **Jira on Atlassian cloud**, use `email`. - - **Password/API token**: Created when you [configured Jira](dvcs.md#configure-jira-for-dvcs). + See [authentication in Jira](index.md#authentication-in-jira). + - **Password/API token**: Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. -1. To enable users to view Jira issues inside the GitLab project, select **Enable Jira issues** and - enter a Jira project key. **(PREMIUM)** + See [authentication in Jira](index.md#authentication-in-jira). +1. To enable users to view Jira issues inside the GitLab project **(PREMIUM)**, select **Enable Jira issues** and + enter a Jira project key. You can display issues only from a single Jira project in a given GitLab project. @@ -127,7 +125,7 @@ To enable the integration in your GitLab project, after you If you enable Jira issues with this setting, all users with access to this GitLab project can view all issues from the specified Jira project. -1. To enable issue creation for vulnerabilities, select **Enable Jira issues creation from vulnerabilities**. +1. To enable issue creation for vulnerabilities **(ULTIMATE)**, select **Enable Jira issues creation from vulnerabilities**. 1. Select the **Jira issue type**. If the dropdown is empty, select refresh (**{retry}**) and try again. 1. To verify the Jira connection is working, select **Test settings**. 1. Select **Save changes**. diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index 046dd125cd1..d69243e50a6 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -7,9 +7,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Jira DVCS connector **(FREE)** Use the Jira DVCS (distributed version control system) connector if you self-host -either your Jira instance or your GitLab instance, and you want to sync information -between them. If you use Jira Cloud and GitLab.com, you should use the -[GitLab for Jira app](connect-app.md) unless you specifically need the DVCS connector. +your Jira instance, and you want to sync information +between GitLab and Jira. If you use Jira Cloud and GitLab.com, you should use the +[GitLab.com for Jira Cloud app](connect-app.md) unless you specifically need the DVCS connector. When you configure the Jira DVCS connector, make sure your GitLab and Jira instances are accessible. @@ -67,8 +67,9 @@ your integration. 1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to use to connect to GitLab. For Jira to access all projects, - a user with [Administrator](../../user/permissions.md) permissions must - create the user. + a user with [administrator](../../user/permissions.md) permissions must + create the user with administrator permissions. +1. Sign in as the `jira` user. 1. In the top right corner, click the account's avatar, and select **Edit profile**. 1. In the left sidebar, select **Applications**. 1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`. @@ -90,9 +91,6 @@ your integration. ## Configure Jira for DVCS -If you use Jira Cloud, use the [GitLab for Jira app](connect-app.md) -unless you specifically need the DVCS Connector. - Configure this connection when you want to import all GitLab commits and branches, for the groups you specify, into Jira. This import takes a few minutes and, after it completes, refreshes every 60 minutes: diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md index 2646a6c5e2e..4219cb915cc 100644 --- a/doc/integration/jira/index.md +++ b/doc/integration/jira/index.md @@ -7,12 +7,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Jira integrations **(FREE)** If your organization uses [Jira](https://www.atlassian.com/software/jira) issues, -you can [migrate](../../user/project/import/jira.md) your issues from Jira and work +you can [migrate your issues from Jira](../../user/project/import/jira.md) **(PREMIUM)** and work exclusively in GitLab. However, if you'd like to continue to use Jira, you can integrate it with GitLab. GitLab offers two types of Jira integrations, and you -can use one or both depending on the capabilities you need. +can use one or both depending on the capabilities you need. It is recommended that you enable both. -## Compare Jira integrations +## Compare integrations After you set up one or both of these integrations, you can cross-reference activity in your GitLab project with any of your projects in Jira. @@ -20,10 +20,13 @@ in your GitLab project with any of your projects in Jira. For an overview, see [Agile Management - GitLab-Jira Basic Integration](https://www.youtube.com/watch?v=fWvwkx5_00E&feature=youtu.be). -### Per-project Jira integration +### Jira integration -This integration connects a single GitLab project to a Jira instance. The Jira instance -can be hosted by you or in [Atlassian cloud](https://www.atlassian.com/cloud): +This integration connects one or more GitLab project to a Jira instance. The Jira instance +can be hosted by you or in [Atlassian cloud](https://www.atlassian.com/cloud). +The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`. +To simplify administration, we recommend that a GitLab group maintainer or group owner +(or instance administrator in the case of self-managed GitLab) set up the integration. - *If your installation uses Jira Cloud,* use the [GitLab for Jira app](connect-app.md). @@ -48,7 +51,7 @@ displays in the [development panel](https://support.atlassian.com/jira-software- | Add Jira time tracking to an issue. | No. | Yes. Time can be specified using Jira Smart Commits. | | Use a Git commit or merge request to transition or close a Jira issue. | Yes. Only a single transition type, typically configured to close the issue by setting it to Done. | Yes. Transition to any state using Jira Smart Commits. | | Display a list of Jira issues. | Yes. **(PREMIUM)** | No. | -| Create a Jira issue from a vulnerability or finding. **(ULTIMATE)** | Yes. | No. | +| Create a Jira issue from a vulnerability or finding. | Yes. **(ULTIMATE)** | No. | ## Authentication in Jira @@ -64,10 +67,10 @@ The process for configuring Jira depends on whether you host Jira on your own se ## Privacy considerations -If you integrate a private GitLab project with Jira using the [**Per-project Jira integration**](#per-project-jira-integration), +If you integrate a private GitLab project with Jira using the [**Jira integration**](#jira-integration), actions in GitLab issues and merge requests linked to a Jira issue leak information about the private project to non-administrator Jira users. If your installation uses Jira Cloud, -you can use the [GitLab for Jira app](connect-app.md) to avoid this risk. +you can use the [GitLab.com for Jira Cloud app](connect-app.md) to avoid this risk. ## Troubleshooting diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md index aafc9956b4e..34ca5481b85 100644 --- a/doc/integration/jira/issues.md +++ b/doc/integration/jira/issues.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Jira integration issue management **(FREE)** -Integrating issue management with Jira requires you to [configure Jira](development_panel.md#configure-the-integration) +Integrating issue management with Jira requires you to [configure Jira](index.md#jira-integration) and [enable the Jira service](development_panel.md#configure-gitlab) in GitLab. After you configure and enable the integration, you can reference and close Jira issues by mentioning the Jira ID in GitLab commits and merge requests. @@ -122,7 +122,7 @@ Issues are grouped into tabs based on their - **Closed** tab: All issues with a Jira status categorized as Done. - **All** tab: All issues of any status. -## Search and filter the issues list +### Search and filter the issues list **(PREMIUM)** To refine the list of issues, use the search bar to search for any text contained in an issue summary (title) or description. Use any combination diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 056feda290b..47016fd244e 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -59,6 +59,7 @@ See also the Code Climate list of [Supported Languages for Maintainability](http > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267612) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.11. > - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/284140) in GitLab 13.12. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284140) in GitLab 14.1. > - [Feature enhanced](https://gitlab.com/gitlab-org/gitlab/-/issues/2526) in GitLab 14.0. Changes to files in merge requests can cause Code Quality to fall if merged. In these cases, diff --git a/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb b/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb index e722cb324cc..e694e5359cd 100644 --- a/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb +++ b/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb @@ -172,19 +172,17 @@ module Gitlab # Updates rows in merge_request_diff_commits with their new # commit_author_id and committer_id values. def update_commit_rows(to_update, user_mapping) - MergeRequestDiffCommitUser.transaction do - to_update.each_slice(UPDATES_PER_QUERY) do |slice| - updates = {} + to_update.each_slice(UPDATES_PER_QUERY) do |slice| + updates = {} - slice.each do |(diff_id, order), (author, committer)| - author_id = user_mapping[author]&.id - committer_id = user_mapping[committer]&.id + slice.each do |(diff_id, order), (author, committer)| + author_id = user_mapping[author]&.id + committer_id = user_mapping[committer]&.id - updates[[diff_id, order]] = [author_id, committer_id] - end - - bulk_update_commit_rows(updates) + updates[[diff_id, order]] = [author_id, committer_id] end + + bulk_update_commit_rows(updates) end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index b9a1eba2b4a..d26a903c1f8 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -22,17 +22,9 @@ module Gitlab ::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false) end - def self.display_quality_on_mr_diff?(project) - ::Feature.enabled?(:codequality_mr_diff, project, default_enabled: :yaml) - end - def self.gldropdown_tags_enabled? ::Feature.enabled?(:gldropdown_tags, default_enabled: :yaml) end - - def self.require_builds_token_encryption? - Feature.enabled?(:ci_builds_tokens_required_encryption, default_enabled: :yaml) - end end end end diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb index e6e7d97d296..2e54e8bfc1a 100644 --- a/lib/gitlab/object_hierarchy.rb +++ b/lib/gitlab/object_hierarchy.rb @@ -65,32 +65,9 @@ module Gitlab # Note: By default the order is breadth-first # rubocop: disable CodeReuse/ActiveRecord def base_and_ancestors(upto: nil, hierarchy_order: nil) - if use_distinct? - expose_depth = hierarchy_order.present? - hierarchy_order ||= :asc - - # if hierarchy_order is given, the calculated `depth` should be present in SELECT - if expose_depth - recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all).distinct - read_only(unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)).order(depth: hierarchy_order)) - else - recursive_query = base_and_ancestors_cte(upto).apply_to(unscoped_model.all) - - if skip_ordering? - recursive_query = recursive_query.distinct - else - recursive_query = recursive_query.reselect(*recursive_query.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct - recursive_query = unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)) - recursive_query = remove_depth_and_maintain_order(recursive_query, hierarchy_order: hierarchy_order) - end - - read_only(recursive_query) - end - else - recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all) - recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order - read_only(recursive_query) - end + recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all) + recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order + read_only(recursive_query) end # rubocop: enable CodeReuse/ActiveRecord @@ -101,27 +78,7 @@ module Gitlab # and incremented as we go down the descendant tree # rubocop: disable CodeReuse/ActiveRecord def base_and_descendants(with_depth: false) - if use_distinct? - # Always calculate `depth`, remove it later if with_depth is false - if with_depth - base_cte = base_and_descendants_cte(with_depth: true).apply_to(unscoped_model.all).distinct - read_only(unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)).order(depth: :asc)) - else - base_cte = base_and_descendants_cte.apply_to(unscoped_model.all) - - if skip_ordering? - base_cte = base_cte.distinct - else - base_cte = base_cte.reselect(*base_cte.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct - base_cte = unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)) - base_cte = remove_depth_and_maintain_order(base_cte, hierarchy_order: :asc) - end - - read_only(base_cte) - end - else - read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(unscoped_model.all)) - end + read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(unscoped_model.all)) end # rubocop: enable CodeReuse/ActiveRecord @@ -158,11 +115,6 @@ module Gitlab ancestors_scope = unscoped_model.from(ancestors_table) descendants_scope = unscoped_model.from(descendants_table) - if use_distinct? - ancestors_scope = ancestors_scope.distinct - descendants_scope = descendants_scope.distinct - end - relation = unscoped_model .with .recursive(ancestors.to_arel, descendants.to_arel) @@ -177,23 +129,6 @@ module Gitlab private - # Use distinct on the Namespace queries to avoid bad planner behavior in PG11. - def use_distinct? - return unless model <= Namespace - # Global use_distinct_for_all_object_hierarchy takes precedence over use_distinct_in_object_hierarchy - return true if Feature.enabled?(:use_distinct_for_all_object_hierarchy) - return options[:use_distinct] if options.key?(:use_distinct) - - false - end - - # Skips the extra ordering when using distinct on the namespace queries - def skip_ordering? - return options[:skip_ordering] if options.key?(:skip_ordering) - - false - end - # Remove the extra `depth` field using an INNER JOIN to avoid breaking UNION queries # and ordering the rows based on the `depth` column to maintain the row order. # diff --git a/spec/controllers/users/unsubscribes_controller_spec.rb b/spec/controllers/users/unsubscribes_controller_spec.rb new file mode 100644 index 00000000000..5670c951e59 --- /dev/null +++ b/spec/controllers/users/unsubscribes_controller_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::UnsubscribesController do + let!(:user) { create :user, email: 'me@example.com' } + + describe "show" do + it "responds with success" do + get :show, params: { email: Base64.urlsafe_encode64('me@example.com') } + + assert_response :success + end + + it "behaves the same if email address isn't known in the system" do + get :show, params: { email: Base64.urlsafe_encode64('i@dont_exists.com') } + + assert_response :success + end + end + + describe "create" do + it "unsubscribes the connected user" do + post :create, params: { email: Base64.urlsafe_encode64('me@example.com') } + + assert user.reload.admin_email_unsubscribed_at + end + + # Don't tell if the email does not exists + it "behaves the same if email address isn't known in the system" do + post :create, params: { email: Base64.urlsafe_encode64('i@dont_exists.com') } + + assert_response :redirect + end + end +end diff --git a/spec/graphql/features/feature_flag_spec.rb b/spec/graphql/features/feature_flag_spec.rb index 30238cf9cb3..e5560fccf89 100644 --- a/spec/graphql/features/feature_flag_spec.rb +++ b/spec/graphql/features/feature_flag_spec.rb @@ -28,14 +28,25 @@ RSpec.describe 'Graphql Field feature flags' do end end - it 'returns the value when feature is enabled' do - expect(subject['item']).to eq('name' => test_object.name) + it 'checks YAML definition for default_enabled' do + # Exception is indicative of a check for YAML definition + expect { subject }.to raise_error(Feature::InvalidFeatureFlagError, /The feature flag YAML definition for '#{feature_flag}' does not exist/) end - it 'returns nil when the feature is disabled' do - stub_feature_flags(feature_flag => false) + context 'skipping YAML check' do + before do + skip_default_enabled_yaml_check + end - expect(subject).to be_nil + it 'returns the value when feature is enabled' do + expect(subject['item']).to eq('name' => test_object.name) + end + + it 'returns nil when the feature is disabled' do + stub_feature_flags(feature_flag => false) + + expect(subject).to be_nil + end end end end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 54b59317b55..ebdb3299bc9 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -130,14 +130,25 @@ RSpec.describe Types::BaseField do skip_feature_flags_yaml_validation end - it 'returns false if the feature is not enabled' do - stub_feature_flags(flag => false) - - expect(field.visible?(context)).to eq(false) + it 'checks YAML definition for default_enabled' do + # Exception is indicative of a check for YAML definition + expect { field.visible?(context) }.to raise_error(Feature::InvalidFeatureFlagError, /The feature flag YAML definition for '#{flag}' does not exist/) end - it 'returns true if the feature is enabled' do - expect(field.visible?(context)).to eq(true) + context 'skipping YAML check' do + before do + skip_default_enabled_yaml_check + end + + it 'returns false if the feature is not enabled' do + stub_feature_flags(flag => false) + + expect(field.visible?(context)).to eq(false) + end + + it 'returns true if the feature is enabled' do + expect(field.visible?(context)).to eq(true) + end end end end diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb index 7615b37521a..64161fbafdd 100644 --- a/spec/lib/gitlab/object_hierarchy_spec.rb +++ b/spec/lib/gitlab/object_hierarchy_spec.rb @@ -9,265 +9,178 @@ RSpec.describe Gitlab::ObjectHierarchy do let(:options) { {} } - shared_context 'Gitlab::ObjectHierarchy test cases' do - describe '#base_and_ancestors' do + describe '#base_and_ancestors' do + let(:relation) do + described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors + end + + it 'includes the base rows' do + expect(relation).to include(child2) + end + + it 'includes all of the ancestors' do + expect(relation).to include(parent, child1) + end + + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1) + + expect(relation).to contain_exactly(child2) + end + + it 'uses ancestors_base #initialize argument' do + relation = described_class.new(Group.where(id: child2.id), Group.none, options: options).base_and_ancestors + + expect(relation).to include(parent, child1, child2) + end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end + + describe 'hierarchy_order option' do let(:relation) do - described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors + described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors(hierarchy_order: hierarchy_order) end - it 'includes the base rows' do - expect(relation).to include(child2) - end + context ':asc' do + let(:hierarchy_order) { :asc } - it 'includes all of the ancestors' do - expect(relation).to include(parent, child1) - end - - it 'can find ancestors upto a certain level' do - relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1) - - expect(relation).to contain_exactly(child2) - end - - it 'uses ancestors_base #initialize argument' do - relation = described_class.new(Group.where(id: child2.id), Group.none, options: options).base_and_ancestors - - expect(relation).to include(parent, child1, child2) - end - - it 'does not allow the use of #update_all' do - expect { relation.update_all(share_with_group_lock: false) } - .to raise_error(ActiveRecord::ReadOnlyRecord) - end - - describe 'hierarchy_order option' do - let(:relation) do - described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors(hierarchy_order: hierarchy_order) - end - - context ':asc' do - let(:hierarchy_order) { :asc } - - it 'orders by child to parent' do - expect(relation).to eq([child2, child1, parent]) - end - end - - context ':desc' do - let(:hierarchy_order) { :desc } - - it 'orders by parent to child' do - expect(relation).to eq([parent, child1, child2]) - end + it 'orders by child to parent' do + expect(relation).to eq([child2, child1, parent]) end end - end - describe '#base_and_descendants' do - let(:relation) do - described_class.new(Group.where(id: parent.id), options: options).base_and_descendants - end + context ':desc' do + let(:hierarchy_order) { :desc } - it 'includes the base rows' do - expect(relation).to include(parent) - end - - it 'includes all the descendants' do - expect(relation).to include(child1, child2) - end - - it 'uses descendants_base #initialize argument' do - relation = described_class.new(Group.none, Group.where(id: parent.id), options: options).base_and_descendants - - expect(relation).to include(parent, child1, child2) - end - - it 'does not allow the use of #update_all' do - expect { relation.update_all(share_with_group_lock: false) } - .to raise_error(ActiveRecord::ReadOnlyRecord) - end - - context 'when with_depth is true' do - let(:relation) do - described_class.new(Group.where(id: parent.id), options: options).base_and_descendants(with_depth: true) - end - - it 'includes depth in the results' do - object_depths = { - parent.id => 1, - child1.id => 2, - child2.id => 3 - } - - relation.each do |object| - expect(object.depth).to eq(object_depths[object.id]) - end - end - end - end - - describe '#descendants' do - it 'includes only the descendants' do - relation = described_class.new(Group.where(id: parent), options: options).descendants - - expect(relation).to contain_exactly(child1, child2) - end - end - - describe '#max_descendants_depth' do - subject { described_class.new(base_relation, options: options).max_descendants_depth } - - context 'when base relation is empty' do - let(:base_relation) { Group.where(id: nil) } - - it { expect(subject).to be_nil } - end - - context 'when base has no children' do - let(:base_relation) { Group.where(id: child2) } - - it { expect(subject).to eq(1) } - end - - context 'when base has grandchildren' do - let(:base_relation) { Group.where(id: parent) } - - it { expect(subject).to eq(3) } - end - end - - describe '#ancestors' do - it 'includes only the ancestors' do - relation = described_class.new(Group.where(id: child2), options: options).ancestors - - expect(relation).to contain_exactly(child1, parent) - end - - it 'can find ancestors upto a certain level' do - relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1) - - expect(relation).to be_empty - end - end - - describe '#all_objects' do - let(:relation) do - described_class.new(Group.where(id: child1.id), options: options).all_objects - end - - it 'includes the base rows' do - expect(relation).to include(child1) - end - - it 'includes the ancestors' do - expect(relation).to include(parent) - end - - it 'includes the descendants' do - expect(relation).to include(child2) - end - - it 'uses ancestors_base #initialize argument for ancestors' do - relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id), options: options).all_objects - - expect(relation).to include(parent) - end - - it 'uses descendants_base #initialize argument for descendants' do - relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id), options: options).all_objects - - expect(relation).to include(child2) - end - - it 'does not allow the use of #update_all' do - expect { relation.update_all(share_with_group_lock: false) } - .to raise_error(ActiveRecord::ReadOnlyRecord) - end - end - end - - context 'when the use_distinct_in_object_hierarchy feature flag is enabled' do - before do - stub_feature_flags(use_distinct_in_object_hierarchy: true) - stub_feature_flags(use_distinct_for_all_object_hierarchy: false) - end - - it_behaves_like 'Gitlab::ObjectHierarchy test cases' - - it 'calls DISTINCT' do - expect(child2.self_and_ancestors.to_sql).to include("DISTINCT") - end - - context 'when use_traversal_ids feature flag is enabled' do - it 'does not call DISTINCT' do - expect(parent.self_and_descendants.to_sql).not_to include("DISTINCT") - end - end - - context 'when use_traversal_ids feature flag is disabled' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - it 'calls DISTINCT' do - expect(parent.self_and_descendants.to_sql).to include("DISTINCT") - end - end - end - - context 'when the use_distinct_for_all_object_hierarchy feature flag is enabled' do - before do - stub_feature_flags(use_distinct_in_object_hierarchy: false) - stub_feature_flags(use_distinct_for_all_object_hierarchy: true) - end - - it_behaves_like 'Gitlab::ObjectHierarchy test cases' - - it 'calls DISTINCT' do - expect(child2.self_and_ancestors.to_sql).to include("DISTINCT") - end - - context 'when use_traversal_ids feature flag is enabled' do - it 'does not call DISTINCT' do - expect(parent.self_and_descendants.to_sql).not_to include("DISTINCT") - end - end - - context 'when use_traversal_ids feature flag is disabled' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - it 'calls DISTINCT' do - expect(parent.self_and_descendants.to_sql).to include("DISTINCT") - end - - context 'when the skip_ordering option is set' do - let(:options) { { skip_ordering: true } } - - it_behaves_like 'Gitlab::ObjectHierarchy test cases' - - it 'does not include ROW_NUMBER()' do - query = described_class.new(Group.where(id: parent.id), options: options).base_and_descendants.to_sql - - expect(query).to include("DISTINCT") - expect(query).not_to include("ROW_NUMBER()") + it 'orders by parent to child' do + expect(relation).to eq([parent, child1, child2]) end end end end - context 'when the use_distinct_in_object_hierarchy feature flag is disabled' do - before do - stub_feature_flags(use_distinct_in_object_hierarchy: false) - stub_feature_flags(use_distinct_for_all_object_hierarchy: false) + describe '#base_and_descendants' do + let(:relation) do + described_class.new(Group.where(id: parent.id), options: options).base_and_descendants end - it_behaves_like 'Gitlab::ObjectHierarchy test cases' + it 'includes the base rows' do + expect(relation).to include(parent) + end - it 'does not call DISTINCT' do - expect(parent.self_and_descendants.to_sql).not_to include("DISTINCT") - expect(child2.self_and_ancestors.to_sql).not_to include("DISTINCT") + it 'includes all the descendants' do + expect(relation).to include(child1, child2) + end + + it 'uses descendants_base #initialize argument' do + relation = described_class.new(Group.none, Group.where(id: parent.id), options: options).base_and_descendants + + expect(relation).to include(parent, child1, child2) + end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end + + context 'when with_depth is true' do + let(:relation) do + described_class.new(Group.where(id: parent.id), options: options).base_and_descendants(with_depth: true) + end + + it 'includes depth in the results' do + object_depths = { + parent.id => 1, + child1.id => 2, + child2.id => 3 + } + + relation.each do |object| + expect(object.depth).to eq(object_depths[object.id]) + end + end + end + end + + describe '#descendants' do + it 'includes only the descendants' do + relation = described_class.new(Group.where(id: parent), options: options).descendants + + expect(relation).to contain_exactly(child1, child2) + end + end + + describe '#max_descendants_depth' do + subject { described_class.new(base_relation, options: options).max_descendants_depth } + + context 'when base relation is empty' do + let(:base_relation) { Group.where(id: nil) } + + it { expect(subject).to be_nil } + end + + context 'when base has no children' do + let(:base_relation) { Group.where(id: child2) } + + it { expect(subject).to eq(1) } + end + + context 'when base has grandchildren' do + let(:base_relation) { Group.where(id: parent) } + + it { expect(subject).to eq(3) } + end + end + + describe '#ancestors' do + it 'includes only the ancestors' do + relation = described_class.new(Group.where(id: child2), options: options).ancestors + + expect(relation).to contain_exactly(child1, parent) + end + + it 'can find ancestors upto a certain level' do + relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1) + + expect(relation).to be_empty + end + end + + describe '#all_objects' do + let(:relation) do + described_class.new(Group.where(id: child1.id), options: options).all_objects + end + + it 'includes the base rows' do + expect(relation).to include(child1) + end + + it 'includes the ancestors' do + expect(relation).to include(parent) + end + + it 'includes the descendants' do + expect(relation).to include(child2) + end + + it 'uses ancestors_base #initialize argument for ancestors' do + relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id), options: options).all_objects + + expect(relation).to include(parent) + end + + it 'uses descendants_base #initialize argument for descendants' do + relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id), options: options).all_objects + + expect(relation).to include(child2) + end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) end end end diff --git a/spec/mailers/emails/admin_notification_spec.rb b/spec/mailers/emails/admin_notification_spec.rb new file mode 100644 index 00000000000..90381eb8ffd --- /dev/null +++ b/spec/mailers/emails/admin_notification_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Emails::AdminNotification do + it 'adds email methods to Notify' do + subject.instance_methods.each do |email_method| + expect(Notify).to be_respond_to(email_method) + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 3b1694527d3..ae956adf563 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1609,6 +1609,32 @@ RSpec.describe Notify do end end end + + describe 'admin notification' do + let(:example_site_path) { root_path } + let(:user) { create(:user) } + + subject { @email = described_class.send_admin_notification(user.id, 'Admin announcement', 'Text') } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq("GitLab") + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to recipient' do + is_expected.to deliver_to user.email + end + + it 'has the correct subject' do + is_expected.to have_subject 'Admin announcement' + end + + it 'includes unsubscribe link' do + unsubscribe_link = "http://localhost/unsubscribes/#{Base64.urlsafe_encode64(user.email)}" + is_expected.to have_body_text(unsubscribe_link) + end + end end describe 'confirmation if email changed' do diff --git a/spec/migrations/20210604133651_schedule_merge_request_diff_users_background_migration_spec.rb b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb similarity index 73% rename from spec/migrations/20210604133651_schedule_merge_request_diff_users_background_migration_spec.rb rename to spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb index 923e97520d2..9cc454662f9 100644 --- a/spec/migrations/20210604133651_schedule_merge_request_diff_users_background_migration_spec.rb +++ b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require 'spec_helper' -require_migration! 'schedule_merge_request_diff_users_background_migration' +require_migration! 'reschedule_merge_request_diff_users_background_migration' -RSpec.describe ScheduleMergeRequestDiffUsersBackgroundMigration, :migration do +RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration, :migration do let(:migration) { described_class.new } describe '#up' do @@ -19,6 +19,21 @@ RSpec.describe ScheduleMergeRequestDiffUsersBackgroundMigration, :migration do .and_return(85_123) end + it 'deletes existing background migration job records' do + args = [150_000, 300_000] + + Gitlab::Database::BackgroundMigrationJob + .create!(class_name: described_class::MIGRATION_NAME, arguments: args) + + migration.up + + found = Gitlab::Database::BackgroundMigrationJob + .where(class_name: described_class::MIGRATION_NAME, arguments: args) + .count + + expect(found).to eq(0) + end + it 'schedules the migrations in batches' do expect(migration) .to receive(:migrate_in) diff --git a/spec/migrations/reset_job_token_scope_enabled_spec.rb b/spec/migrations/reset_job_token_scope_enabled_spec.rb new file mode 100644 index 00000000000..40dfe4de34b --- /dev/null +++ b/spec/migrations/reset_job_token_scope_enabled_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe ResetJobTokenScopeEnabled do + let(:settings) { table(:project_ci_cd_settings) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id)} + let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id)} + + before do + settings.create!(id: 1, project_id: project_1.id, job_token_scope_enabled: true) + settings.create!(id: 2, project_id: project_2.id, job_token_scope_enabled: false) + end + + it 'migrates job_token_scope_enabled to be always false' do + expect { migrate! } + .to change { settings.where(job_token_scope_enabled: false).count } + .from(1).to(2) + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index cb9ce851137..74a476a6422 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3811,16 +3811,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'can generate a codequality report' do expect(subject).to be_truthy end - - context 'when feature is disabled' do - before do - stub_feature_flags(codequality_mr_diff: false) - end - - it 'can not generate a codequality report' do - expect(subject).to be_falsey - end - end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 554d2b0751d..edd543854cb 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2083,14 +2083,6 @@ RSpec.describe MergeRequest, factory_default: :keep do let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports) } it { is_expected.to be_truthy } - - context 'when feature flag is disabled' do - before do - stub_feature_flags(codequality_mr_diff: false) - end - - it { is_expected.to be_falsey } - end end context 'when head pipeline does not have codeqquality mr diff report' do diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 5b215db8570..6e5d7725a7a 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -294,32 +294,6 @@ module Ci end end - context 'when the use_distinct_in_register_job_object_hierarchy feature flag is enabled' do - before do - stub_feature_flags(use_distinct_in_register_job_object_hierarchy: true) - stub_feature_flags(use_distinct_for_all_object_hierarchy: true) - end - - it 'calls DISTINCT' do - queue = ::Ci::Queue::BuildQueueService.new(group_runner) - - expect(queue.builds_for_group_runner.to_sql).to include("DISTINCT") - end - end - - context 'when the use_distinct_in_register_job_object_hierarchy feature flag is disabled' do - before do - stub_feature_flags(use_distinct_in_register_job_object_hierarchy: false) - stub_feature_flags(use_distinct_for_all_object_hierarchy: false) - end - - it 'does not call DISTINCT' do - queue = ::Ci::Queue::BuildQueueService.new(group_runner) - - expect(queue.builds_for_group_runner.to_sql).not_to include("DISTINCT") - end - end - context 'group runner' do let(:build) { execute(group_runner) }