From eb9f5ce5d9ec0c5dd4954ffe448214ae8eb144ac Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 14 Oct 2023 00:11:11 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop.yml | 6 + app/models/project_authorization.rb | 1 + app/models/todo.rb | 1 + app/services/todos/destroy/base_service.rb | 31 --- .../destroy/confidential_issue_service.rb | 37 ++-- .../todos/destroy/group_private_service.rb | 29 +-- .../todos/destroy/project_private_service.rb | 27 ++- .../destroy/unauthorized_features_service.rb | 8 + ...058_drop_index_namespaces_on_updated_at.rb | 18 ++ db/schema_migrations/20231011200058 | 1 + db/structure.sql | 2 - doc/api/users.md | 2 +- .../merge_request_concepts/diffs/frontend.md | 190 +++++++++++++++++- doc/integration/facebook.md | 16 +- .../automate_runner_creation/index.md | 4 +- doc/tutorials/boards_for_teams/index.md | 15 +- doc/tutorials/compliance_pipeline/index.md | 4 +- .../index.md | 2 +- .../create_register_first_runner/index.md | 2 +- doc/tutorials/dependency_scanning.md | 4 +- doc/tutorials/export_sbom.md | 2 +- doc/tutorials/hugo/index.md | 2 +- .../install_gitlab_single_node/index.md | 2 +- doc/tutorials/issue_triage/index.md | 2 +- doc/tutorials/make_first_git_commit/index.md | 4 +- doc/tutorials/manage_user/index.md | 4 +- doc/tutorials/protected_workflow/index.md | 2 +- doc/tutorials/scan_execution_policy/index.md | 4 +- doc/tutorials/scan_result_policy/index.md | 14 +- doc/tutorials/update_commit_messages/index.md | 2 +- .../website_project_with_analytics/index.md | 8 +- doc/user/project/wiki/index.md | 18 ++ .../migration/create_table_migration.rb.tt | 3 + .../active_record/migration/migration.rb.tt | 3 + .../post_deployment_migration/migration.rb.tt | 3 + ...d_background_migration_dictionary.template | 3 + rubocop/batched_background_migrations.rb | 32 +++ .../cop/migration/unfinished_dependencies.rb | 51 +++++ ...y_batched_migration_dictionary_matcher.txt | 3 + .../generators/model/mocks/migration_file.txt | 3 + spec/models/project_authorization_spec.rb | 26 ++- spec/models/todo_spec.rb | 13 ++ .../batched_background_migrations_spec.rb | 43 ++++ spec/rubocop/check_graceful_task_spec.rb | 7 +- .../migration/unfinished_dependencies_spec.rb | 118 +++++++++++ spec/rubocop_spec_helper.rb | 2 + 46 files changed, 641 insertions(+), 133 deletions(-) create mode 100644 db/post_migrate/20231011200058_drop_index_namespaces_on_updated_at.rb create mode 100644 db/schema_migrations/20231011200058 create mode 100644 rubocop/batched_background_migrations.rb create mode 100644 rubocop/cop/migration/unfinished_dependencies.rb create mode 100644 spec/rubocop/batched_background_migrations_spec.rb create mode 100644 spec/rubocop/cop/migration/unfinished_dependencies_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 3f2cf15e46d..544ef66fba6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -466,6 +466,12 @@ Migration/BatchMigrationsPostOnly: - 'db/migrate/*.rb' - 'db/post_migrate/*.rb' +Migration/UnfinishedDependencies: + Enabled: true + Include: + - 'db/migrate/*.rb' + - 'db/post_migrate/*.rb' + BackgroundMigration/FeatureCategory: Enabled: true Include: diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index c328e7d37c8..4d0c6029235 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -11,6 +11,7 @@ class ProjectAuthorization < ApplicationRecord validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: :project }, presence: true + scope :for_project, ->(projects) { where(project: projects) } scope :non_guests, -> { where('access_level > ?', ::Gitlab::Access::GUEST) } # TODO: To be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/418205 diff --git a/app/models/todo.rb b/app/models/todo.rb index 9d5a6289781..c49ed1093e1 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -80,6 +80,7 @@ class Todo < ApplicationRecord scope :for_type, -> (type) { where(target_type: type) } scope :for_target, -> (id) { where(target_id: id) } scope :for_commit, -> (id) { where(commit_id: id) } + scope :not_in_users, -> (user_ids) { where.not('todos.user_id' => user_ids) } scope :with_entity_associations, -> do preload(:target, :author, :note, group: :route, project: [:route, :group, { namespace: [:route, :owner] }, :project_setting]) end diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index ed5c4df85b1..c9c86330e1c 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -4,37 +4,6 @@ module Todos module Destroy class BaseService def execute - return unless todos_to_remove? - - ::Gitlab::Database.allow_cross_joins_across_databases(url: - 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do - without_authorized(todos).delete_all - end - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def without_authorized(items) - items.where.not('todos.user_id' => authorized_users) - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def authorized_users - ProjectAuthorization.select(:user_id).where(project_id: project_ids) - end - # rubocop: enable CodeReuse/ActiveRecord - - def todos - raise NotImplementedError - end - - def project_ids - raise NotImplementedError - end - - def todos_to_remove? raise NotImplementedError end end diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index fadc76b1181..261def314d4 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -13,54 +13,57 @@ module Todos attr_reader :issues - # rubocop: disable CodeReuse/ActiveRecord def initialize(issue_id: nil, project_id: nil) @issues = if issue_id - Issue.where(id: issue_id) + Issue.id_in(issue_id) elsif project_id project_confidential_issues(project_id) end end - # rubocop: enable CodeReuse/ActiveRecord + + def execute + return unless todos_to_remove? + + ::Gitlab::Database.allow_cross_joins_across_databases( + url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422045') do + delete_todos + end + end private + def delete_todos + authorized_users = ProjectAuthorization.select(:user_id) + .for_project(project_ids) + .non_guests + + todos.not_in_users(authorized_users).delete_all + end + def project_confidential_issues(project_id) project = Project.find(project_id) project.issues.confidential_only end - override :todos # rubocop: disable CodeReuse/ActiveRecord def todos Todo.joins_issue_and_assignees - .where(target: issues) - .where(issues: { confidential: true }) + .for_target(issues) + .merge(Issue.confidential_only) .where('todos.user_id != issues.author_id') .where('todos.user_id != issue_assignees.user_id') end # rubocop: enable CodeReuse/ActiveRecord - override :todos_to_remove? def todos_to_remove? issues&.any?(&:confidential?) end - override :project_ids def project_ids issues&.distinct&.select(:project_id) end - - override :authorized_users - # rubocop: disable CodeReuse/ActiveRecord - def authorized_users - ProjectAuthorization.select(:user_id) - .where(project_id: project_ids) - .where('access_level >= ?', Gitlab::Access::REPORTER) - end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index 60599ca9ca4..4230d763209 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -7,30 +7,31 @@ module Todos attr_reader :group - # rubocop: disable CodeReuse/ActiveRecord def initialize(group_id) - @group = Group.find_by(id: group_id) + @group = Group.find_by_id(group_id) + end + + def execute + return unless todos_to_remove? + + delete_todos end - # rubocop: enable CodeReuse/ActiveRecord private - override :todos - # rubocop: disable CodeReuse/ActiveRecord - def todos - Todo.where(group_id: group.id) - end - # rubocop: enable CodeReuse/ActiveRecord - - override :authorized_users - def authorized_users - User.from_union([ + def delete_todos + authorized_users = User.from_union([ group.project_users_with_descendants.select(:id), group.members_with_parents.select(:user_id) ], remove_duplicates: false) + + todos.not_in_users(authorized_users).delete_all + end + + def todos + Todo.for_group(group.id) end - override :todos_to_remove? def todos_to_remove? group&.private? end diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index e00d10c3780..a1ca0d8543c 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -7,27 +7,32 @@ module Todos attr_reader :project - # rubocop: disable CodeReuse/ActiveRecord def initialize(project_id) - @project = Project.find_by(id: project_id) + @project = Project.find_by_id(project_id) + end + + def execute + return unless todos_to_remove? + + delete_todos end - # rubocop: enable CodeReuse/ActiveRecord private - override :todos - # rubocop: disable CodeReuse/ActiveRecord - def todos - Todo.where(project_id: project.id) - end - # rubocop: enable CodeReuse/ActiveRecord + def delete_todos + authorized_users = ProjectAuthorization.select(:user_id).for_project(project_ids) + + todos.not_in_users(authorized_users).delete_all + end + + def todos + Todo.for_project(project.id) + end - override :project_ids def project_ids project.id end - override :todos_to_remove? def todos_to_remove? project&.private? end diff --git a/app/services/todos/destroy/unauthorized_features_service.rb b/app/services/todos/destroy/unauthorized_features_service.rb index 513def10575..22f7a0b2a37 100644 --- a/app/services/todos/destroy/unauthorized_features_service.rb +++ b/app/services/todos/destroy/unauthorized_features_service.rb @@ -27,6 +27,14 @@ module Todos private + def without_authorized(items) + items.not_in_users(authorized_users) + end + + def authorized_users + ProjectAuthorization.select(:user_id).for_project(project_ids) + end + def related_todos base_scope = Todo.for_project(project_id) base_scope = base_scope.for_user(user_id) if user_id diff --git a/db/post_migrate/20231011200058_drop_index_namespaces_on_updated_at.rb b/db/post_migrate/20231011200058_drop_index_namespaces_on_updated_at.rb new file mode 100644 index 00000000000..68787442931 --- /dev/null +++ b/db/post_migrate/20231011200058_drop_index_namespaces_on_updated_at.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class DropIndexNamespacesOnUpdatedAt < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + TABLE_NAME = :namespaces + INDEX_NAME = :index_namespaces_on_updated_at + + def up + remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME + end + + def down + # no-op + # Since adding the same index will be time consuming, + # we have to create it asynchronously using 'prepare_async_index' helper. + end +end diff --git a/db/schema_migrations/20231011200058 b/db/schema_migrations/20231011200058 new file mode 100644 index 00000000000..fbd04ba6342 --- /dev/null +++ b/db/schema_migrations/20231011200058 @@ -0,0 +1 @@ +0437aade771694981f1eca79c1bd8fed0857f1f10f1a616684fab3c906bd20b7 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index bf2af226227..0d6de24b545 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -33199,8 +33199,6 @@ CREATE INDEX index_namespaces_on_traversal_ids_for_groups_btree ON namespaces US CREATE INDEX index_namespaces_on_type_and_id ON namespaces USING btree (type, id); -CREATE INDEX index_namespaces_on_updated_at ON namespaces USING btree (updated_at); - CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20)); CREATE INDEX index_namespaces_sync_events_on_namespace_id ON namespaces_sync_events USING btree (namespace_id); diff --git a/doc/api/users.md b/doc/api/users.md index ba96eaa2bb4..8d270710b20 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -2174,7 +2174,7 @@ Example response: "user_id": 42, "active": true, "expires_at": "2020-10-15", - "token": "ggbfKkC4n-Lujy8jwCR2" + "token": "" } ``` diff --git a/doc/development/merge_request_concepts/diffs/frontend.md b/doc/development/merge_request_concepts/diffs/frontend.md index ff163050e1f..bafd0b5e437 100644 --- a/doc/development/merge_request_concepts/diffs/frontend.md +++ b/doc/development/merge_request_concepts/diffs/frontend.md @@ -23,10 +23,192 @@ The Vue app for rendering diffs uses many different Vue components, some of whic with other areas of the GitLab app. The below chart shows the direction for which components get rendered. -NOTE: -[Issue #388843](https://gitlab.com/gitlab-org/gitlab/-/issues/388843) is open to -generate a Mermaid graph of the components diagram. An image version of the -diagram is available in the issue. +This chart contains several types of items: + +| Legend item | Interpretation | +| ----------- | -------------- | +| `xxx~~`, `ee-xxx~~` | A shortened directory path name. Can be found in `[ee]/app/assets/javascripts`, and omits `0..n` nested folders. | +| Rectangular nodes | Files. | +| Oval nodes | Plain language describing a deeper concept. | +| Double-rectangular nodes | Simplified code branch. | +| Diamond and circle nodes | Branches that have 2 (diamond) or 3+ (circle) options. | +| Pendant / banner nodes (left notch, right square) | A parent directory to shorten nested paths. | +| `./` | A path relative to the closest parent directory pendant node. Non-relative paths nested under parent pendant nodes are not in that directory. | + +```mermaid + flowchart TB + classDef code font-family: monospace; + + A["diffs~~app.vue"] + descVirtualScroller(["Virtual Scroller"]) + codeForFiles[["v-for(diffFiles)"]] + B["diffs~~diff_file.vue"] + C["diffs~~diff_file_header.vue"] + D["diffs~~diff_stats.vue"] + E["diffs~~diff_content.vue"] + boolFileIsText{isTextFile} + boolOnlyWhitespace{isWhitespaceOnly} + boolNotDiffable{notDiffable} + boolNoPreview{noPreview} + descShowChanges(["Show button to "Show changes""]) + %% Non-text changes + dirDiffViewer>"vue_shared~~diff_viewer"] + F["./viewers/not_diffable.vue"] + G["./viewers/no_preview.vue"] + H["./diff_viewer.vue"] + I["diffs~~diff_view.vue"] + boolIsRenamed{isRenamed} + boolIsModeChanged{isModeChanged} + boolFileHasNoPath{hasNewPath} + boolIsImage{isImage} + J["./viewers/renamed.vue"] + K["./viewers/mode_changed.vue"] + descNoViewer(["No viewer is rendered"]) + L["./viewers/image_diff_viewer.vue"] + M["./viewers/download.vue"] + N["vue_shared~~download_diff_viewer.vue"] + boolImageIsReplaced{isReplaced} + O["vue_shared~~image_viewer.vue"] + switchImageMode((image_diff_viewer.mode)) + P["./viewers/image_diff/onion_skin_viewer.vue"] + Q["./viewers/image_diff/swipe_viewer.vue"] + R["./viewers/image_diff/two_up_viewer.vue"] + S["diffs~~image_diff_overlay.vue"] + codeForImageDiscussions[["v-for(discussions)"]] + T["vue_shared~~design_note_pin.vue"] + U["vue_shared~~user_avatar_link.vue"] + V["diffs~~diff_discussions.vue"] + W["batch_comments~~diff_file_drafts.vue"] + codeForTwoUpDiscussions[["v-for(discussions)"]] + codeForTwoUpDrafts[["v-for(drafts)"]] + X["notes~~notable_discussion.vue"] + %% Text-file changes + codeForDiffLines[["v-for(diffLines)"]] + Y["diffs~~diff_expansion_cell.vue"] + Z["diffs~~diff_row.vue"] + AA["diffs~~diff_line.vue"] + AB["batch_comments~~draft_note.vue"] + AC["diffs~~diff_comment_cell.vue"] + AD["diffs~~diff_gutter_avatars.vue"] + AE["ee-diffs~~inline_findings_flag_switcher.vue"] + AF["notes~~noteable_note.vue"] + AG["unknown~~publish_button.vue"] + AH["notes~~note_actions.vue"] + AI["notes~~note_body.vue"] + AJ["notes~~note_header.vue"] + AK["notes~~reply_button.vue"] + AL["notes~~note_awards_list.vue"] + AM["notes~~note_edited_text.vue"] + AN["notes~~note_form.vue"] + AO["vue_shared~~awards_list.vue"] + AP["emoji~~picker.vue"] + AQ["emoji~~emoji_list.vue"] + descEmojiVirtualScroll(["Virtual Scroller"]) + AR["emoji~~category.vue"] + AS["emoji~emoji_category.vue"] + AT["vue_shared~~markdown_editor.vue"] + + class codeForFiles,codeForImageDiscussions code; + class codeForTwoUpDiscussions,codeForTwoUpDrafts code; + class codeForDiffLines code; + %% Also apply code styling to this switch node + class switchImageMode code; + %% Also apply code styling to these boolean nodes + class boolFileIsText,boolOnlyWhitespace,boolNotDiffable,boolNoPreview code; + class boolIsRenamed,boolIsModeChanged,boolFileHasNoPath,boolIsImage code; + class boolImageIsReplaced code; + + A --> descVirtualScroller + A -->|"Virtual Scroller is + disabled when + Find in page search + (Cmd/Ctrl+f) is used."|codeForFiles + descVirtualScroller --> codeForFiles + codeForFiles --> B + B --> C + C --> D + B --> E + + %% File view flags cascade + E --> boolFileIsText + boolFileIsText --> |yes| I + boolFileIsText --> |no| boolOnlyWhitespace + + boolOnlyWhitespace --> |yes| descShowChanges + boolOnlyWhitespace --> |no| dirDiffViewer + + dirDiffViewer --> H + + H --> boolNotDiffable + + boolNotDiffable --> |yes| F + boolNotDiffable --> |no| boolNoPreview + + boolNoPreview --> |yes| G + boolNoPreview --> |no| boolIsRenamed + + boolIsRenamed --> |yes| J + boolIsRenamed --> |no| boolIsModeChanged + + boolIsModeChanged --> |yes| K + boolIsModeChanged --> |no| boolFileHasNoPath + + boolFileHasNoPath --> |yes| boolIsImage + boolFileHasNoPath --> |no| descNoViewer + + boolIsImage --> |yes| L + boolIsImage --> |no| M + M --> N + + %% Image diff viewer + L --> boolImageIsReplaced + + boolImageIsReplaced --> |yes| switchImageMode + boolImageIsReplaced --> |no| O + + switchImageMode -->|"'twoup' (default)"| R + switchImageMode -->|'onion'| P + switchImageMode -->|'swipe'| Q + + P & Q --> S + S --> codeForImageDiscussions + S --> AN + + R-->|"Rendered in + note container div"|U & W & V + R --> S + + V --> codeForTwoUpDiscussions + W --> codeForTwoUpDrafts + + %% This invisible link forces `noteable_discussion` + %% to render above `design_note_pin` + X ~~~ T + + codeForTwoUpDrafts --> AB + codeForImageDiscussions & codeForTwoUpDiscussions & codeForTwoUpDrafts --> T + codeForTwoUpDiscussions --> X + + %% Text file diff viewer + I --> codeForDiffLines + codeForDiffLines --> Z + codeForDiffLines -->|"isMatchLine?"| Y + codeForDiffLines -->|"hasCodeQuality?"| AA + codeForDiffLines -->|"hasDraftNote(s)?"| AB + + Z -->|"hasCodeQuality?"| AE + Z -->|"hasDiscussions?"| AD + + AA --> AC + + %% Draft notes + AB --> AF + AF --> AH & AI & AJ + AH --> AK + AI --> AL & AM & AN + AL --> AO --> AP --> AQ --> descEmojiVirtualScroll --> AR --> AS + AN --> AT +``` Some of the components are rendered more than others, but the main component is `diff_row.vue`. This component renders every diff line in a diff file. For performance reasons, this diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index 2366005ca55..0fc66f2abd4 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -62,13 +62,15 @@ To enable the Facebook OmniAuth provider, you must: 1. On your GitLab server, open the configuration file: - - For Linux package installations: + ::Tabs + + :::TabTitle Linux package installations ```shell sudo editor /etc/gitlab/gitlab.rb ``` - - For self-compiled installations: + :::TabTitle Self-compiled installations ```shell cd /home/git/gitlab @@ -76,13 +78,17 @@ To enable the Facebook OmniAuth provider, you must: sudo -u git -H editor config/gitlab.yml ``` + ::EndTabs + 1. Configure the [common settings](omniauth.md#configure-common-settings) to add `facebook` as a single sign-on provider. This enables Just-In-Time account provisioning for users who do not have an existing GitLab account. 1. Add the provider configuration: - - For Linux package installations: + ::Tabs + + :::TabTitle Linux package installations ```ruby gitlab_rails['omniauth_providers'] = [ @@ -95,7 +101,7 @@ To enable the Facebook OmniAuth provider, you must: ] ``` - - For self-compiled installations: + :::TabTitle Self-compiled installations ```yaml - { name: 'facebook', @@ -104,6 +110,8 @@ To enable the Facebook OmniAuth provider, you must: app_secret: 'YOUR_APP_SECRET' } ``` + ::EndTabs + 1. In the provide configuration, paste the following values: 1. `YOUR_APP_ID`: The **App ID** you copied in the previous step. diff --git a/doc/tutorials/automate_runner_creation/index.md b/doc/tutorials/automate_runner_creation/index.md index fa1373f1e3a..1241b760e2f 100644 --- a/doc/tutorials/automate_runner_creation/index.md +++ b/doc/tutorials/automate_runner_creation/index.md @@ -21,7 +21,7 @@ with runner authentication tokens, which have replaced the deprecated registrati method that uses registration tokens. For more information, see [The new runner registration workflow](../../ci/runners/new_creation_workflow.md#the-new-runner-registration-workflow). -## Prerequisites +## Before you begin - GitLab Runner must be installed on your GitLab instance. - To create shared runners, you must be an administrator. @@ -97,7 +97,7 @@ To create a runner configuration, you can use: ### With the GitLab REST API -Prerequisites: +Before you begin, you need: - The URL for your GitLab instance. For example, if your project is hosted on `gitlab.example.com/yourname/yourproject`, your GitLab instance URL is diff --git a/doc/tutorials/boards_for_teams/index.md b/doc/tutorials/boards_for_teams/index.md index fd61a4c03c5..d4c44fb8967 100644 --- a/doc/tutorials/boards_for_teams/index.md +++ b/doc/tutorials/boards_for_teams/index.md @@ -23,6 +23,11 @@ To set up issue boards for multiple teams: 1. [Create team issue boards](#create-team-issue-boards) 1. [Create issues for features](#create-issues-for-features) +## Before you begin + +- If you're using an existing group for this tutorial, make sure you have at least the Reporter role for the group. +- If you're using an existing project for this tutorial, make sure you have at least the Reporter role for the project. + ## The goal workflow After you set up everything, the two teams will be able to hand off issues from one board to another, for example, like this: @@ -55,11 +60,6 @@ To prepare for when your project grows, start by creating a group. You use groups to manage one or more related projects at the same time. You add your users as members in the group, and assign them a role. -Prerequisites: - -- If you're using an existing group for this tutorial, make sure you have at least the Reporter role - for the group. - To create a group: 1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New group**. @@ -75,11 +75,6 @@ The main code development work happens in projects and their repositories. A project contains your code and pipelines, but also the issues that are used for planning your upcoming code changes. -Prerequisites: - -- If you're using an existing project for this tutorial, make sure you have at least the Reporter role - for the project. - To create a blank project: 1. In your group, on the left sidebar, at the top, select **Create new** (**{plus}**) and then select diff --git a/doc/tutorials/compliance_pipeline/index.md b/doc/tutorials/compliance_pipeline/index.md index 710a2eb59ab..bc06e44fd48 100644 --- a/doc/tutorials/compliance_pipeline/index.md +++ b/doc/tutorials/compliance_pipeline/index.md @@ -18,9 +18,9 @@ In this tutorial, you: 1. Create a [new project and apply the compliance framework](#create-a-new-project-and-apply-the-compliance-framework) to it. 1. Combine [compliance pipeline configuration and regular pipeline configuration](#combine-pipeline-configurations). -Prerequisites: +## Before you begin -- Permission to create new top-level groups. +- You need permission to create new top-level groups. ## Create a new group diff --git a/doc/tutorials/configure_gitlab_runner_to_use_gke/index.md b/doc/tutorials/configure_gitlab_runner_to_use_gke/index.md index 8fa2dbe3479..5397fe2871a 100644 --- a/doc/tutorials/configure_gitlab_runner_to_use_gke/index.md +++ b/doc/tutorials/configure_gitlab_runner_to_use_gke/index.md @@ -21,7 +21,7 @@ To configure GitLab Runner to use the GKE: 1. [Install and configure the Kubernetes Operator](#install-and-configure-the-kubernetes-operator). 1. Optional. [Verify that the configuration was successful](#verify-your-configuration). -## Prerequisites +## Before you begin Before you can configure GitLab Runner to use the GKE you must: diff --git a/doc/tutorials/create_register_first_runner/index.md b/doc/tutorials/create_register_first_runner/index.md index a16899509e7..33e9f422125 100644 --- a/doc/tutorials/create_register_first_runner/index.md +++ b/doc/tutorials/create_register_first_runner/index.md @@ -25,7 +25,7 @@ configuration: 1. [Create and register a project runner](#create-and-register-a-project-runner). 1. [Trigger a pipeline to run your runner](#trigger-a-pipeline-to-run-your-runner). -## Prerequisite +## Before you begin Before you can create, register, and run a runner, [GitLab Runner](https://docs.gitlab.com/runner/install/) must be installed on a local computer. diff --git a/doc/tutorials/dependency_scanning.md b/doc/tutorials/dependency_scanning.md index 6eb2592c2f4..c513ce205d8 100644 --- a/doc/tutorials/dependency_scanning.md +++ b/doc/tutorials/dependency_scanning.md @@ -23,9 +23,9 @@ To set up dependency scanning: - [Resolve the high severity vulnerability](#resolve-the-high-severity-vulnerability). - [Test vulnerability detection in a merge request](#test-vulnerability-detection-in-a-merge-request). -## Prerequisites +## Before you begin -Before you begin, make sure you have GitPod enabled. GitPod is an on-demand cloud development +Make sure you have GitPod enabled. GitPod is an on-demand cloud development environment. For details, see [GitPod](../integration/gitpod.md). Alternatively you can use your own development setup. In this case you need to have Yarn and Node.js installed. diff --git a/doc/tutorials/export_sbom.md b/doc/tutorials/export_sbom.md index f0bbf6febf6..716140045fb 100644 --- a/doc/tutorials/export_sbom.md +++ b/doc/tutorials/export_sbom.md @@ -10,7 +10,7 @@ Dependency Scanning output can be exported to the CycloneDX JSON format. This tutorial shows you how to generate a CycloneDX JSON SBOM for a pipeline, and then to upload it as a CI job artifact. -## Prerequisites +## Before you begin Set up Dependency Scanning. For detailed instructions, follow [the Dependency Scanning tutorial](dependency_scanning.md). diff --git a/doc/tutorials/hugo/index.md b/doc/tutorials/hugo/index.md index 5f466234a36..cd2bc32e94b 100644 --- a/doc/tutorials/hugo/index.md +++ b/doc/tutorials/hugo/index.md @@ -20,7 +20,7 @@ Here's an overview of what you're going to do: 1. Build your Hugo site with a CI/CD pipeline. 1. Deploy and view your Hugo site with GitLab Pages. -## Prerequisites +## Before you begin - An account on GitLab.com. - Familiarity with Git. diff --git a/doc/tutorials/install_gitlab_single_node/index.md b/doc/tutorials/install_gitlab_single_node/index.md index 5ed98ccbefc..5b81e10ddb1 100644 --- a/doc/tutorials/install_gitlab_single_node/index.md +++ b/doc/tutorials/install_gitlab_single_node/index.md @@ -17,7 +17,7 @@ To install a single node GitLab instance and configure it to be secure: 1. [Configure GitLab](#configure-gitlab) 1. [Next steps](#next-steps) -## Prerequisites +## Before you begin - A domain name, and a correct [setup of DNS](https://docs.gitlab.com/omnibus/settings/dns.html). - A Debian-based server with the following minimum specs: diff --git a/doc/tutorials/issue_triage/index.md b/doc/tutorials/issue_triage/index.md index 2634837edc8..fec0e0f82c3 100644 --- a/doc/tutorials/issue_triage/index.md +++ b/doc/tutorials/issue_triage/index.md @@ -24,7 +24,7 @@ To set up GitLab for issue triage in a project: 1. [Create an issue triage board](#create-an-issue-triage-board) 1. [Create issues for features](#create-issues-for-features) -## Prerequisites +## Before you begin - If you're using an existing project for this tutorial, make sure you have at least the Reporter role for the project. diff --git a/doc/tutorials/make_first_git_commit/index.md b/doc/tutorials/make_first_git_commit/index.md index 8099fc65f14..92bf2572078 100644 --- a/doc/tutorials/make_first_git_commit/index.md +++ b/doc/tutorials/make_first_git_commit/index.md @@ -12,9 +12,7 @@ committing changes to a Git repository from the command line. When you're done, you'll have a project where you can practice using Git. -## What you need - -Before you begin: +## Before you begin - [Install Git on your local machine](../../topics/git/how_to_install_git/index.md). - Ensure you can sign in to an instance of GitLab. If your organization doesn't diff --git a/doc/tutorials/manage_user/index.md b/doc/tutorials/manage_user/index.md index df343fe5f08..b60fd300b5d 100644 --- a/doc/tutorials/manage_user/index.md +++ b/doc/tutorials/manage_user/index.md @@ -35,9 +35,9 @@ You're going to create: 1. A project in the organization for a specific piece of work, and add users to that project. -Prerequisites: +## Before you begin -- You have administrator access to your self-managed GitLab instance. +- Make sure you have administrator access to your self-managed GitLab instance. ## Create the organization parent group and subgroups diff --git a/doc/tutorials/protected_workflow/index.md b/doc/tutorials/protected_workflow/index.md index 5ff798fce6b..64138b15315 100644 --- a/doc/tutorials/protected_workflow/index.md +++ b/doc/tutorials/protected_workflow/index.md @@ -24,7 +24,7 @@ release branches, and creates a minimal approval workflow for the project: 1. [Enforce CODEOWNER approval on branches](#enforce-codeowner-approval-on-branches) 1. [Create the release branches](#create-the-release-branches) -## Prerequisites +## Before you begin - You must have the Maintainer or Owner role. - You need a list of managers and their email addresses. diff --git a/doc/tutorials/scan_execution_policy/index.md b/doc/tutorials/scan_execution_policy/index.md index d0122908e19..e9c58b07c85 100644 --- a/doc/tutorials/scan_execution_policy/index.md +++ b/doc/tutorials/scan_execution_policy/index.md @@ -20,9 +20,9 @@ In this tutorial, you: - [Link project B to the security policy project](#link-project-b-to-the-security-policy-project). - [Test the scan execution policy with project B](#test-the-scan-execution-policy-with-project-b). -Prerequisite: +## Before you begin -- Permission to create new projects in an existing group. +- You need permission to create new projects in an existing group. ## Create project A diff --git a/doc/tutorials/scan_result_policy/index.md b/doc/tutorials/scan_result_policy/index.md index dabc311135a..46005bd8e56 100644 --- a/doc/tutorials/scan_result_policy/index.md +++ b/doc/tutorials/scan_result_policy/index.md @@ -9,19 +9,19 @@ info: To determine the technical writer assigned to the Stage/Group associated w This tutorial shows you how to create and configure a [scan result policy](../../user/application_security/policies/scan-result-policies.md). These policies can be set to take action based on scan results. For example, in this tutorial, you'll set up a policy that requires approval from two specified users if a vulnerability is detected in a merge request. -Prerequisites: - -The namespace used for this tutorial must: - -- Contain a minimum of three users, including your own. If you don't have two other users, you must first - create them. For details, see [Creating users](../../user/profile/account/create_accounts.md). - To set up a scan result policy: 1. [Create a test project](#create-a-test-project). 1. [Add a scan result policy](#add-a-scan-result-policy). 1. [Test the scan result policy](#test-the-scan-result-policy). +## Before you begin + +The namespace used for this tutorial must: + +- Contain a minimum of three users, including your own. If you don't have two other users, you must first + create them. For details, see [Creating users](../../user/profile/account/create_accounts.md). + ## Create a test project 1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New project/repository**. diff --git a/doc/tutorials/update_commit_messages/index.md b/doc/tutorials/update_commit_messages/index.md index 90626d95c32..76acba95e04 100644 --- a/doc/tutorials/update_commit_messages/index.md +++ b/doc/tutorials/update_commit_messages/index.md @@ -27,7 +27,7 @@ To rewrite any number of commit messages: 1. [Update the commit messages](#update-the-commit-messages). 1. [Push the changes up to GitLab](#push-the-changes-up-to-gitlab). -## Prerequisites +## Before you begin You must have: diff --git a/doc/tutorials/website_project_with_analytics/index.md b/doc/tutorials/website_project_with_analytics/index.md index c75ec27fd24..90e85d0f88c 100644 --- a/doc/tutorials/website_project_with_analytics/index.md +++ b/doc/tutorials/website_project_with_analytics/index.md @@ -15,10 +15,6 @@ If this list seems long and you're not sure where to start, then this tutorial i Follow along to learn how to set up an example website project, collaborate with other GitLab users, and use project-level analytics reports to evaluate the development of your project. -Prerequisite: - -- You must have the Owner role for the group in which you create the project. - Here's an overview of what we're going to do: 1. Create a project from a template. @@ -28,6 +24,10 @@ Here's an overview of what we're going to do: 1. Create an Insights report. 1. View merge request and issue analytics. +## Before you begin + +- You must have the Owner role for the group in which you create the project. + ## Create a project from a template First of all, you need to create a project in your group. diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index 63adc0d3945..5fa9077f1c7 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -177,6 +177,24 @@ You need at least the Developer role to move a wiki page: change the **Title** from `about` to `/about`. 1. Select **Save changes**. +## Export a wiki page + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414691) in GitLab 16.3 [with a flag](../../../administration/feature_flags.md) named `print_wiki`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. +To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `print_wiki`. +On GitLab.com, this feature is not available. + +You can export a wiki page as a PDF file: + +1. On the left sidebar, select **Search or go to** and find your project or group. +1. Select **Plan > Wiki**. +1. Go to the page you want to export. +1. Select the vertical ellipsis (**{ellipsis_v}**), and then select **Print as PDF**. + +A PDF of the wiki page is created. + ## View history of a wiki page The changes of a wiki page over time are recorded in the wiki's Git repository. diff --git a/generator_templates/active_record/migration/create_table_migration.rb.tt b/generator_templates/active_record/migration/create_table_migration.rb.tt index 0b0cb05249c..e3eae729139 100644 --- a/generator_templates/active_record/migration/create_table_migration.rb.tt +++ b/generator_templates/active_record/migration/create_table_migration.rb.tt @@ -17,6 +17,9 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data # comments: # disable_ddl_transaction! + # Add dependent 'batched_background_migrations.queued_migration_version' values. + # DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = [] + def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> diff --git a/generator_templates/active_record/migration/migration.rb.tt b/generator_templates/active_record/migration/migration.rb.tt index 50d2b018ae7..40481aed6ea 100644 --- a/generator_templates/active_record/migration/migration.rb.tt +++ b/generator_templates/active_record/migration/migration.rb.tt @@ -21,6 +21,9 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data # Visit: https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html # restrict_gitlab_migration gitlab_schema: :gitlab_main + # Add dependent 'batched_background_migrations.queued_migration_version' values. + # DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = [] + <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> diff --git a/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt b/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt index dcc9d1e4563..dbce7eb201e 100644 --- a/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt +++ b/generator_templates/post_deployment_migration/post_deployment_migration/migration.rb.tt @@ -21,6 +21,9 @@ class <%= migration_class_name %> < Gitlab::Database::Migration[<%= Gitlab::Data # Visit: https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html # restrict_gitlab_migration gitlab_schema: :gitlab_main + # Add dependent 'batched_background_migrations.queued_migration_version' values. + # DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = [] + def up end diff --git a/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template b/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template index 9e33d97b5fa..e73bdda64eb 100644 --- a/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template +++ b/lib/generators/batched_background_migration/templates/batched_background_migration_dictionary.template @@ -5,3 +5,6 @@ feature_category: <%= feature_category %> introduced_by_url: # URL of the MR (or issue/commit) that introduced the migration milestone: <%= current_milestone %> queued_migration_version: <%= migration_number %> +# Replace with the approximate date you think it's best to ensure the completion of this BBM. +finalize_after: # yyyy-mm-dd +finalized_by: # version of the migration that ensured this bbm diff --git a/rubocop/batched_background_migrations.rb b/rubocop/batched_background_migrations.rb new file mode 100644 index 00000000000..ce7115e5cd5 --- /dev/null +++ b/rubocop/batched_background_migrations.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module RuboCop + class BatchedBackgroundMigrations + DICTIONARY_BASE_DIR = 'db/docs/batched_background_migrations' + + attr_reader :queued_migration_version + + class << self + def dictionary_data + @dictionary_data ||= Dir.glob("*.yml", base: DICTIONARY_BASE_DIR).each_with_object({}) do |file_name, data| + dictionary = YAML.load_file(File.join(DICTIONARY_BASE_DIR, file_name)) + + next unless dictionary['queued_migration_version'].present? + + data[dictionary['queued_migration_version'].to_s] = { + finalize_after: dictionary['finalize_after'], + finalized_by: dictionary['finalized_by'].to_s + } + end + end + end + + def initialize(queued_migration_version) + @queued_migration_version = queued_migration_version + end + + def finalized_by + self.class.dictionary_data.dig(queued_migration_version.to_s, :finalized_by) + end + end +end diff --git a/rubocop/cop/migration/unfinished_dependencies.rb b/rubocop/cop/migration/unfinished_dependencies.rb new file mode 100644 index 00000000000..1e0741c8411 --- /dev/null +++ b/rubocop/cop/migration/unfinished_dependencies.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative '../../migration_helpers' +require_relative '../../batched_background_migrations' + +module RuboCop + module Cop + module Migration + # Checks if there are any unfinished dependent batched bg migrations + class UnfinishedDependencies < RuboCop::Cop::Base + include MigrationHelpers + + NOT_FINALIZED_MSG = <<-MESSAGE.delete("\n").squeeze(' ').strip + Dependent migration with queued version %{version} is not yet finalized. + Consider finalizing the dependent migration and update it's finalized_by attr in the dictionary. + MESSAGE + + FINALIZED_BY_LATER_MIGRATION_MSG = <<-MESSAGE.delete("\n").squeeze(' ').strip + Dependent migration with queued version %{version} is finalized by later migration, + it has to be finalized before the current migration. + MESSAGE + + def_node_matcher :dependent_migration_versions, <<~PATTERN + (casgn nil? :DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS (array $...)) + PATTERN + + def on_casgn(node) + return unless in_migration?(node) + + migration_version = version(node) + + dependent_migration_versions(node)&.each do |dependent_migration_version_node| + dependent_migration_version = dependent_migration_version_node.value + finalized_by_version = fetch_finalized_by(dependent_migration_version) + + next if finalized_by_version.present? && finalized_by_version.to_s < migration_version.to_s + + msg = finalized_by_version.present? ? FINALIZED_BY_LATER_MIGRATION_MSG : NOT_FINALIZED_MSG + add_offense(node, message: format(msg, version: dependent_migration_version)) + end + end + + private + + def fetch_finalized_by(queued_migration_version) + BatchedBackgroundMigrations.new(queued_migration_version).finalized_by + end + end + end + end +end diff --git a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt index 6d0bb151463..3b166bd4c4c 100644 --- a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt +++ b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt @@ -5,3 +5,6 @@ feature_category: database introduced_by_url: # URL of the MR \(or issue/commit\) that introduced the migration milestone: [0-9\.]+ queued_migration_version: [0-9]+ +# Replace with the approximate date you think it's best to ensure the completion of this BBM. +finalize_after: # yyyy-mm-dd +finalized_by: # version of the migration that ensured this bbm diff --git a/spec/lib/generators/model/mocks/migration_file.txt b/spec/lib/generators/model/mocks/migration_file.txt index 091e086ba65..c9e51e51863 100644 --- a/spec/lib/generators/model/mocks/migration_file.txt +++ b/spec/lib/generators/model/mocks/migration_file.txt @@ -17,6 +17,9 @@ class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.1] # comments: # disable_ddl_transaction! + # Add dependent 'batched_background_migrations.queued_migration_version' values. + # DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = [] + def change create_table :model_generator_test_foos do |t| diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb index 9fed05342aa..a5f29fcbe8b 100644 --- a/spec/models/project_authorization_spec.rb +++ b/spec/models/project_authorization_spec.rb @@ -83,8 +83,10 @@ RSpec.describe ProjectAuthorization, feature_category: :groups_and_projects do end describe 'scopes' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace) } + describe '.non_guests' do - let_it_be(:project) { create(:project) } let_it_be(:project_original_owner_authorization) { project.owner.project_authorizations.first } let_it_be(:project_authorization_guest) { create(:project_authorization, :guest, project: project) } let_it_be(:project_authorization_reporter) { create(:project_authorization, :reporter, project: project) } @@ -100,6 +102,28 @@ RSpec.describe ProjectAuthorization, feature_category: :groups_and_projects do ].map(&:attributes)) end end + + describe '.for_project' do + let_it_be(:project_2) { create(:project, namespace: user.namespace) } + let_it_be(:project_3) { create(:project, namespace: user.namespace) } + + let_it_be(:project_authorization_3) { project_3.project_authorizations.first } + let_it_be(:project_authorization_2) { project_2.project_authorizations.first } + let_it_be(:project_authorization) { project.project_authorizations.first } + + it 'returns all records for the project' do + expect(described_class.for_project(project).map(&:attributes)).to match_array([ + project_authorization + ].map(&:attributes)) + end + + it 'returns all records for multiple projects' do + expect(described_class.for_project([project, project_3]).map(&:attributes)).to match_array([ + project_authorization, + project_authorization_3 + ].map(&:attributes)) + end + end end describe '.insert_all' do diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 2d6a674d3ce..316d1343512 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -396,6 +396,19 @@ RSpec.describe Todo do end end + describe '.not_in_users' do + it 'returns the expected todos' do + user1 = create(:user) + user2 = create(:user) + + todo1 = create(:todo, user: user1) + todo2 = create(:todo, user: user1) + create(:todo, user: user2) + + expect(described_class.not_in_users(user2)).to contain_exactly(todo1, todo2) + end + end + describe '.for_group_ids_and_descendants' do it 'returns the todos for a group and its descendants' do parent_group = create(:group) diff --git a/spec/rubocop/batched_background_migrations_spec.rb b/spec/rubocop/batched_background_migrations_spec.rb new file mode 100644 index 00000000000..a9b99bb466b --- /dev/null +++ b/spec/rubocop/batched_background_migrations_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../rubocop/batched_background_migrations' + +RSpec.describe RuboCop::BatchedBackgroundMigrations, feature_category: :database do + let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" } + let(:migration_version) { 20230307160250 } + let(:finalized_by_version) { 20230307160255 } + let(:bbm_dictionary_data) do + { + migration_job_name: 'TestMigration', + feature_category: :database, + introduced_by_url: 'https://test_url', + milestone: 16.5, + queued_migration_version: migration_version, + finalized_by: finalized_by_version + } + end + + before do + File.open(bbm_dictionary_file_name, 'w') do |file| + file.write(bbm_dictionary_data.stringify_keys.to_yaml) + end + end + + after do + FileUtils.rm(bbm_dictionary_file_name) + end + + subject(:batched_background_migration) { described_class.new(migration_version) } + + describe '#finalized_by' do + it 'returns the finalized_by version of the bbm with given version' do + expect(batched_background_migration.finalized_by).to eq(finalized_by_version.to_s) + end + + it 'returns nothing for non-existing bbm dictionary' do + expect(described_class.new('random').finalized_by).to be_nil + end + end +end diff --git a/spec/rubocop/check_graceful_task_spec.rb b/spec/rubocop/check_graceful_task_spec.rb index 1ab977266dc..aa66643dd8e 100644 --- a/spec/rubocop/check_graceful_task_spec.rb +++ b/spec/rubocop/check_graceful_task_spec.rb @@ -1,14 +1,9 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'stringio' - -require_relative '../support/helpers/next_instance_of' +require 'rubocop_spec_helper' require_relative '../../rubocop/check_graceful_task' RSpec.describe RuboCop::CheckGracefulTask do - include NextInstanceOf - let(:output) { StringIO.new } subject(:task) { described_class.new(output) } diff --git a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb new file mode 100644 index 00000000000..cac48871856 --- /dev/null +++ b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' +require_relative '../../../../rubocop/cop/migration/unfinished_dependencies' + +RSpec.describe RuboCop::Cop::Migration::UnfinishedDependencies, feature_category: :database do + let(:version) { 20230307160250 } + + let(:migration) do + <<~RUBY + class TestMigration < Gitlab::Database::Migration[2.1] + def perform; end + end + RUBY + end + + before do + allow(cop).to receive(:in_migration?).and_return(true) + + allow(cop).to receive(:version).and_return(version) + end + + shared_examples 'migration with rubocop offense' do + it 'registers an offense' do + expect_offense(migration) + end + end + + shared_examples 'migration without any rubocop offense' do + it 'does not register any offense' do + expect_no_offenses(migration) + end + end + + context 'without any dependent batched background migrations' do + it_behaves_like 'migration without any rubocop offense' + end + + context 'with dependent batched background migrations' do + let(:dependent_migration_versions) { [20230307160240] } + + let(:migration) do + <<~RUBY + class TestMigration < Gitlab::Database::Migration[2.1] + DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = #{dependent_migration_versions} + + def perform; end + end + RUBY + end + + context 'with unfinished dependent migration' do + before do + allow(cop).to receive(:fetch_finalized_by) + .with(dependent_migration_versions.first) + .and_return(nil) + end + + it_behaves_like 'migration with rubocop offense' do + let(:migration) do + <<~RUBY + class TestMigration < Gitlab::Database::Migration[2.1] + DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = #{dependent_migration_versions} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format(described_class::NOT_FINALIZED_MSG, version: dependent_migration_versions.first)} + + def perform; end + end + RUBY + end + end + end + + context 'with incorrectly finalized dependent migration' do + let(:dependent_migration_versions) { [20230307160240, 20230307160230] } + + before do + allow(cop).to receive(:fetch_finalized_by) + .with(dependent_migration_versions.first) + .and_return(version - 10) + + allow(cop).to receive(:fetch_finalized_by) + .with(dependent_migration_versions.last) + .and_return(version + 10) + end + + it_behaves_like 'migration with rubocop offense' do + let(:migration) do + <<~RUBY + class TestMigration < Gitlab::Database::Migration[2.1] + DEPENDENT_BATCHED_BACKGROUND_MIGRATIONS = #{dependent_migration_versions} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format(described_class::FINALIZED_BY_LATER_MIGRATION_MSG, version: dependent_migration_versions.last)} + + def perform; end + end + RUBY + end + end + end + + context 'with properly finalized dependent background migrations' do + before do + allow_next_instance_of(RuboCop::BatchedBackgroundMigrations) do |bbms| + allow(bbms).to receive(:finalized_by).and_return(version - 5) + end + end + + it_behaves_like 'migration without any rubocop offense' + end + end + + context 'for non migrations' do + before do + allow(cop).to receive(:in_migration?).and_return(false) + end + + it_behaves_like 'migration without any rubocop offense' + end +end diff --git a/spec/rubocop_spec_helper.rb b/spec/rubocop_spec_helper.rb index 9884cdd0272..2f1dc2843be 100644 --- a/spec/rubocop_spec_helper.rb +++ b/spec/rubocop_spec_helper.rb @@ -8,6 +8,7 @@ require 'fast_spec_helper' require 'rubocop' require 'rubocop/rspec/shared_contexts/default_rspec_language_config_context' +require_relative 'support/helpers/next_instance_of' require_relative 'rubocop/support_workaround' RSpec.configure do |config| @@ -21,6 +22,7 @@ RSpec.configure do |config| config.include RuboCop::RSpec::ExpectOffense, type: :rubocop config.include RuboCop::RSpec::ExpectOffense, type: :rubocop_rspec + config.include NextInstanceOf config.include_context 'config', type: :rubocop config.include_context 'with default RSpec/Language config', type: :rubocop_rspec