diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js index ee97714824e..81ddf8d77fa 100644 --- a/app/assets/javascripts/editor/source_editor.js +++ b/app/assets/javascripts/editor/source_editor.js @@ -1,7 +1,6 @@ -import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; +import { editor as monacoEditor, Uri } from 'monaco-editor'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import languages from '~/ide/lib/languages'; -import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import { registerLanguages } from '~/ide/utils'; import { joinPaths } from '~/lib/utils/url_utility'; import { uuids } from '~/lib/utils/uuids'; @@ -11,7 +10,7 @@ import { EDITOR_READY_EVENT, EDITOR_TYPE_DIFF, } from './constants'; -import { clearDomElement } from './utils'; +import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils'; export default class SourceEditor { constructor(options = {}) { @@ -22,26 +21,11 @@ export default class SourceEditor { ...options, }; - SourceEditor.setupMonacoTheme(); + setupEditorTheme(); registerLanguages(...languages); } - static setupMonacoTheme() { - const themeName = window.gon?.user_color_scheme || DEFAULT_THEME; - const theme = themes.find((t) => t.name === themeName); - if (theme) monacoEditor.defineTheme(themeName, theme.data); - monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); - } - - static getModelLanguage(path) { - const ext = `.${path.split('.').pop()}`; - const language = monacoLanguages - .getLanguages() - .find((lang) => lang.extensions.indexOf(ext) !== -1); - return language ? language.id : 'plaintext'; - } - static pushToImportsArray(arr, toImport) { arr.push(import(toImport)); } @@ -124,10 +108,7 @@ export default class SourceEditor { return model; } const diffModel = { - original: monacoEditor.createModel( - blobOriginalContent, - SourceEditor.getModelLanguage(model.uri.path), - ), + original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)), modified: model, }; instance.setModel(diffModel); @@ -155,7 +136,7 @@ export default class SourceEditor { }; static instanceUpdateLanguage(inst, path) { - const lang = SourceEditor.getModelLanguage(path); + const lang = getBlobLanguage(path); const model = inst.getModel(); return monacoEditor.setModelLanguage(model, lang); } diff --git a/app/assets/javascripts/editor/utils.js b/app/assets/javascripts/editor/utils.js index af4473413f4..6977db161e0 100644 --- a/app/assets/javascripts/editor/utils.js +++ b/app/assets/javascripts/editor/utils.js @@ -1,3 +1,6 @@ +import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor'; +import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; + export const clearDomElement = (el) => { if (!el || !el.firstChild) return; @@ -6,6 +9,22 @@ export const clearDomElement = (el) => { } }; -export default () => ({ - clearDomElement, -}); +export const setupEditorTheme = () => { + const themeName = window.gon?.user_color_scheme || DEFAULT_THEME; + const theme = themes.find((t) => t.name === themeName); + if (theme) monacoEditor.defineTheme(themeName, theme.data); + monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); +}; + +export const getBlobLanguage = (path) => { + const ext = `.${path.split('.').pop()}`; + const language = monacoLanguages + .getLanguages() + .find((lang) => lang.extensions.indexOf(ext) !== -1); + return language ? language.id : 'plaintext'; +}; + +export const setupCodeSnippet = (el) => { + monacoEditor.colorizeElement(el); + setupEditorTheme(); +}; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue index 1dc40f57efb..4d6a1d5462b 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue @@ -29,8 +29,10 @@ export default { }, computed: { showMetadata() { - return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes( - this.packageEntity.packageType, + return ( + [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes( + this.packageEntity.packageType, + ) && this.packageEntity.metadata ); }, showNugetMetadata() { diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue index 4ab64350d26..3d3fa62fd43 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue @@ -1,8 +1,4 @@ + + diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue index c4fd97188de..f8f7482422e 100644 --- a/app/assets/javascripts/terraform/components/states_table_actions.vue +++ b/app/assets/javascripts/terraform/components/states_table_actions.vue @@ -8,12 +8,14 @@ import { GlIcon, GlModal, GlSprintf, + GlModalDirective, } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql'; import lockState from '../graphql/mutations/lock_state.mutation.graphql'; import removeState from '../graphql/mutations/remove_state.mutation.graphql'; import unlockState from '../graphql/mutations/unlock_state.mutation.graphql'; +import InitCommandModal from './init_command_modal.vue'; export default { components: { @@ -25,6 +27,10 @@ export default { GlIcon, GlModal, GlSprintf, + InitCommandModal, + }, + directives: { + GlModalDirective, }, props: { state: { @@ -36,6 +42,7 @@ export default { return { showRemoveModal: false, removeConfirmText: '', + showCommandModal: false, }; }, i18n: { @@ -54,6 +61,7 @@ export default { remove: s__('Terraform|Remove state file and versions'), removeSuccessful: s__('Terraform|%{name} successfully removed'), unlock: s__('Terraform|Unlock'), + copyCommand: s__('Terraform|Copy Terraform init command'), }, computed: { cancelModalProps() { @@ -74,6 +82,9 @@ export default { attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }], }; }, + commandModalId() { + return `init-command-modal-${this.state.name}`; + }, }, methods: { hideModal() { @@ -164,6 +175,9 @@ export default { }); }); }, + copyInitCommand() { + this.showCommandModal = true; + }, }, }; @@ -181,6 +195,14 @@ export default { + + {{ $options.i18n.copyCommand }} + + + + diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js index 3f986423836..1b8cab0d51e 100644 --- a/app/assets/javascripts/terraform/index.js +++ b/app/assets/javascripts/terraform/index.js @@ -24,11 +24,16 @@ export default () => { }, }); - const { emptyStateImage, projectPath } = el.dataset; + const { emptyStateImage, projectPath, accessTokensPath, terraformApiUrl, username } = el.dataset; return new Vue({ el, apolloProvider: new VueApollo({ defaultClient }), + provide: { + accessTokensPath, + terraformApiUrl, + username, + }, render(createElement) { return createElement(TerraformList, { props: { diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index 0808f364e87..8e1438eaf8a 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -184,6 +184,21 @@ body.gl-dark { } } } + + .gl-datepicker-theme { + .pika-prev, + .pika-next { + filter: invert(0.9); + } + + .is-selected > .pika-button { + color: $gray-900; + } + + :not(.is-selected) > .pika-button:hover { + background-color: $gray-200; + } + } } $border-white-normal: $border-color; diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 982234f7506..75623d33ef5 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -3,6 +3,7 @@ class GroupMembersFinder < UnionFinder RELATIONS = %i(direct inherited descendants).freeze DEFAULT_RELATIONS = %i(direct inherited).freeze + INVALID_RELATION_TYPE_ERROR_MSG = "is not a valid relation type. Valid relation types are #{RELATIONS.join(', ')}." RELATIONS_DESCRIPTIONS = { direct: 'Members in the group itself', @@ -42,6 +43,8 @@ class GroupMembersFinder < UnionFinder attr_reader :user, :group def groups_by_relations(include_relations) + check_relation_arguments!(include_relations) + case include_relations.sort when [:inherited] group.ancestors @@ -86,6 +89,12 @@ class GroupMembersFinder < UnionFinder def members_of_groups(groups) GroupMember.non_request.of_groups(groups) end + + def check_relation_arguments!(include_relations) + unless include_relations & RELATIONS == include_relations + raise ArgumentError, "#{(include_relations - RELATIONS).first} #{INVALID_RELATION_TYPE_ERROR_MSG}" + end + end end GroupMembersFinder.prepend_mod_with('GroupMembersFinder') diff --git a/app/helpers/projects/terraform_helper.rb b/app/helpers/projects/terraform_helper.rb index 621d97ffb69..fb35224fad3 100644 --- a/app/helpers/projects/terraform_helper.rb +++ b/app/helpers/projects/terraform_helper.rb @@ -5,7 +5,10 @@ module Projects::TerraformHelper { empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'), project_path: project.full_path, - terraform_admin: current_user&.can?(:admin_terraform_state, project) + terraform_admin: current_user&.can?(:admin_terraform_state, project), + access_tokens_path: profile_personal_access_tokens_path, + username: current_user&.username, + terraform_api_url: "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state" } end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5cc8474b71d..7758620f605 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -891,7 +891,7 @@ module Ci end def valid_dependency? - return false if artifacts_expired? + return false if artifacts_expired? && !pipeline.artifacts_locked? return false if erased? true diff --git a/app/models/integration.rb b/app/models/integration.rb index a9c865569d0..5c4d03f1fa8 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -274,7 +274,7 @@ class Integration < ApplicationRecord end def self.closest_group_integration(type, scope) - group_ids = scope.ancestors(hierarchy_order: :asc).select(:id) + group_ids = scope.ancestors.select(:id) array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]' where(type: type, group_id: group_ids, inherit_from_id: nil) diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 3216ec42427..081e51c1028 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -178,10 +178,6 @@ module Namespaces depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))" skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth") .order(depth: hierarchy_order) - # The SELECT includes an extra depth attribute. We then wrap the SQL - # in a standard SELECT to avoid mismatched attribute errors when - # trying to chain future ActiveRelation commands. - skope = self.class.without_sti_condition.from(skope, self.class.table_name) end skope diff --git a/app/models/project.rb b/app/models/project.rb index 9f90cef9c12..85bb1dea48f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -914,9 +914,7 @@ class Project < ApplicationRecord .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order) end - def ancestors(hierarchy_order: nil) - namespace&.self_and_ancestors(hierarchy_order: hierarchy_order) - end + alias_method :ancestors, :ancestors_upto def ancestors_upto_ids(...) ancestors_upto(...).pluck(:id) diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml index 2e1590cb826..42c31b3272f 100644 --- a/app/views/projects/packages/packages/show.html.haml +++ b/app/views/projects/packages/packages/show.html.haml @@ -6,7 +6,7 @@ .row .col-12 - - if Feature.enabled?(:package_details_apollo) + - if Feature.enabled?(:package_details_apollo, default_enabled: :yaml) #js-vue-packages-detail-new{ data: package_details_data(@project, @package) } - else #js-vue-packages-detail{ data: package_details_data(@project, @package, true) } diff --git a/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml b/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml index 1a247d3b2af..f073e94e322 100644 --- a/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml +++ b/config/feature_flags/development/ci_pending_builds_maintain_ci_minutes_data.yml @@ -1,7 +1,7 @@ --- name: ci_pending_builds_maintain_ci_minutes_data introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64443 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338149 milestone: '14.2' type: development group: group::pipeline execution diff --git a/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml b/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml index 5a8b89edfad..16b318509dc 100644 --- a/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml +++ b/config/feature_flags/development/ci_pending_builds_maintain_shared_runners_data.yml @@ -1,7 +1,7 @@ --- name: ci_pending_builds_maintain_shared_runners_data -introduced_by_url: -rollout_issue_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64644 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338152 milestone: '14.1' type: development group: group::pipeline execution diff --git a/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml b/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml index 6eefaeea3a1..326beaf6740 100644 --- a/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml +++ b/config/feature_flags/development/ci_queueing_denormalize_shared_runners_information.yml @@ -1,7 +1,7 @@ --- name: ci_queueing_denormalize_shared_runners_information -introduced_by_url: -rollout_issue_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66082 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338289 milestone: '14.2' type: development group: group::pipeline execution diff --git a/config/feature_flags/development/instance_level_integration_overrides.yml b/config/feature_flags/development/instance_level_integration_overrides.yml index f99b85b3c05..6b1f3dd4276 100644 --- a/config/feature_flags/development/instance_level_integration_overrides.yml +++ b/config/feature_flags/development/instance_level_integration_overrides.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66723 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336750 milestone: '14.2' type: development -group: group::ecosystem +group: group::integrations default_enabled: false diff --git a/config/feature_flags/development/jira_issue_details_edit_labels.yml b/config/feature_flags/development/jira_issue_details_edit_labels.yml index bccd7374907..c43d01bf969 100644 --- a/config/feature_flags/development/jira_issue_details_edit_labels.yml +++ b/config/feature_flags/development/jira_issue_details_edit_labels.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65298 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335069 milestone: '14.1' type: development -group: group::ecosystem +group: group::integrations default_enabled: false diff --git a/config/feature_flags/development/jira_issue_details_edit_status.yml b/config/feature_flags/development/jira_issue_details_edit_status.yml index 9d64707a79f..311e243c570 100644 --- a/config/feature_flags/development/jira_issue_details_edit_status.yml +++ b/config/feature_flags/development/jira_issue_details_edit_status.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628 milestone: '14.1' type: development -group: group::ecosystem +group: group::integrations default_enabled: false diff --git a/config/feature_flags/development/package_details_apollo.yml b/config/feature_flags/development/package_details_apollo.yml index aa8ee47df0c..fbab4c2c7c8 100644 --- a/config/feature_flags/development/package_details_apollo.yml +++ b/config/feature_flags/development/package_details_apollo.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334786 milestone: '14.1' type: development group: group::package -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/report_on_long_redis_durations.yml b/config/feature_flags/development/report_on_long_redis_durations.yml new file mode 100644 index 00000000000..0f93c591d63 --- /dev/null +++ b/config/feature_flags/development/report_on_long_redis_durations.yml @@ -0,0 +1,8 @@ +--- +name: report_on_long_redis_durations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67512 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183 +milestone: '14.2' +type: development +group: team::Scalability +default_enabled: false diff --git a/config/feature_flags/development/web_hooks_disable_failed.yml b/config/feature_flags/development/web_hooks_disable_failed.yml index a54034d73e8..3a7c85edafc 100644 --- a/config/feature_flags/development/web_hooks_disable_failed.yml +++ b/config/feature_flags/development/web_hooks_disable_failed.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329849 milestone: '13.12' type: development -group: group::ecosystem +group: group::integrations default_enabled: false diff --git a/config/feature_flags/ops/api_kaminari_count_with_limit.yml b/config/feature_flags/ops/api_kaminari_count_with_limit.yml index a987d5c65b1..a55e3e67710 100644 --- a/config/feature_flags/ops/api_kaminari_count_with_limit.yml +++ b/config/feature_flags/ops/api_kaminari_count_with_limit.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23 rollout_issue_url: milestone: '11.8' type: ops -group: group::ecosystem +group: group::integrations default_enabled: false diff --git a/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png b/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png deleted file mode 100644 index 7d619b6ad7e..00000000000 Binary files a/doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png and /dev/null differ diff --git a/doc/user/infrastructure/terraform_state.md b/doc/user/infrastructure/terraform_state.md index 57db2b47de4..d6c0c9ac6de 100644 --- a/doc/user/infrastructure/terraform_state.md +++ b/doc/user/infrastructure/terraform_state.md @@ -83,6 +83,14 @@ local machine, this is a simple way to get started: -backend-config="retry_wait_min=5" ``` +If you already have a GitLab-managed Terraform state, you can use the `terraform init` command +with the prepopulated parameters values: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Infrastructure > Terraform**. +1. Next to the environment you want to use, select the [Actions menu](#managing-state-files) + **{ellipsis_v}** and select **Copy Terraform init command**. + You can now run `terraform plan` and `terraform apply` as you normally would. ### Get started using GitLab CI @@ -222,7 +230,7 @@ An example setup is shown below: ```plaintext example_remote_state_address=https://gitlab.com/api/v4/projects//terraform/state/ example_username= - example_access_token= + example_access_token= ``` 1. Define the data source by adding the following code block in a `.tf` file (such as `data.tf`): @@ -362,10 +370,8 @@ contains these fields: state file is locked. - **Pipeline**: A link to the most recent pipeline and its status. - **Details**: Information about when the state file was created or changed. -- **Actions**: Actions you can take on the state file, including downloading, - locking, unlocking, or [removing](#remove-a-state-file) the state file and versions: - - ![Terraform state list](img/terraform_list_view_actions_v13_8.png) +- **Actions**: Actions you can take on the state file, including copying the `terraform init` command, + downloading, locking, unlocking, or [removing](#remove-a-state-file) the state file and versions. NOTE: Additional improvements to the diff --git a/lib/api/entities/ci/job_request/dependency.rb b/lib/api/entities/ci/job_request/dependency.rb index 2c6ed417714..2672a4a245b 100644 --- a/lib/api/entities/ci/job_request/dependency.rb +++ b/lib/api/entities/ci/job_request/dependency.rb @@ -6,7 +6,7 @@ module API module JobRequest class Dependency < Grape::Entity expose :id, :name, :token - expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? } + expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.available_artifacts? } end end end diff --git a/lib/gitlab/checks/changes_access.rb b/lib/gitlab/checks/changes_access.rb index a1c2f8d8280..9ecc93f871b 100644 --- a/lib/gitlab/checks/changes_access.rb +++ b/lib/gitlab/checks/changes_access.rb @@ -48,16 +48,28 @@ module Gitlab commits_by_id = commits.index_by(&:id) result = [] - pending = [newrev] + pending = Set[newrev] # We go up the parent chain of our newrev and collect all commits which # are new. In case a commit's ID cannot be found in the set of new # commits, then it must already be a preexisting commit. - pending.each do |rev| - commit = commits_by_id[rev] + while pending.any? + rev = pending.first + pending.delete(rev) + + # Remove the revision from commit candidates such that we don't walk + # it multiple times. If the hash doesn't contain the revision, then + # we have either already walked the commit or it's not new. + commit = commits_by_id.delete(rev) next if commit.nil? - pending.push(*commit.parent_ids) + # Only add the parent ID to the pending set if we actually know its + # commit to guards us against readding an ID which we have already + # queued up before. + commit.parent_ids.each do |parent_id| + pending.add(parent_id) if commits_by_id.has_key?(parent_id) + end + result << commit end diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb index 8a64abb9f62..0f21a16793d 100644 --- a/lib/gitlab/instrumentation/redis_interceptor.rb +++ b/lib/gitlab/instrumentation/redis_interceptor.rb @@ -5,8 +5,21 @@ module Gitlab module RedisInterceptor APDEX_EXCLUDE = %w[brpop blpop brpoplpush bzpopmin bzpopmax xread xreadgroup].freeze + # These are temporary to help with investigating + # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183 + DURATION_ERROR_THRESHOLD = 1.25.seconds + + class MysteryRedisDurationError < StandardError + attr_reader :backtrace + + def initialize(backtrace) + @backtrace = backtrace + end + end + def call(*args, &block) start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined + start_real_time = Time.now instrumentation_class.instance_count_request instrumentation_class.redis_cluster_validate!(args.first) @@ -27,6 +40,13 @@ module Gitlab instrumentation_class.add_duration(duration) instrumentation_class.add_call_details(duration, args) end + + if duration > DURATION_ERROR_THRESHOLD && Feature.enabled?(:report_on_long_redis_durations, default_enabled: :yaml) + Gitlab::ErrorTracking.track_exception(MysteryRedisDurationError.new(caller), + command: command_from_args(args), + duration: duration, + timestamp: start_real_time.iso8601(5)) + end end def write(command) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b1db6f42ac0..7c36f531da8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11513,7 +11513,7 @@ msgstr "" msgid "DevopsAdoption|Edit subgroups" msgstr "" -msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}." +msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Data is updated at the beginning of each month. Last updated: %{timestamp}." msgstr "" msgid "DevopsAdoption|Fuzz Testing" @@ -32641,6 +32641,9 @@ msgstr "" msgid "Terraform|Cancel" msgstr "" +msgid "Terraform|Copy Terraform init command" +msgstr "" + msgid "Terraform|Details" msgstr "" @@ -32692,12 +32695,18 @@ msgstr "" msgid "Terraform|States" msgstr "" +msgid "Terraform|Terraform init command" +msgstr "" + msgid "Terraform|The report %{name} failed to generate." msgstr "" msgid "Terraform|The report %{name} was generated in your pipelines." msgstr "" +msgid "Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}." +msgstr "" + msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:" msgstr "" diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb index 752303fdd78..9a7950266a5 100644 --- a/spec/features/groups/packages_spec.rb +++ b/spec/features/groups/packages_spec.rb @@ -52,6 +52,8 @@ RSpec.describe 'Group Packages' do it_behaves_like 'package details link' end + it_behaves_like 'package details link' + it 'allows you to navigate to the project page' do find('[data-testid="root-link"]', text: project.name).click diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb index fa4c57c305d..30298f79312 100644 --- a/spec/features/projects/packages_spec.rb +++ b/spec/features/projects/packages_spec.rb @@ -45,6 +45,8 @@ RSpec.describe 'Packages' do it_behaves_like 'package details link' end + it_behaves_like 'package details link' + context 'deleting a package' do let_it_be(:project) { create(:project) } let_it_be(:package) { create(:package, project: project) } diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb index d080d101285..2c63f2bfc02 100644 --- a/spec/features/projects/terraform_spec.rb +++ b/spec/features/projects/terraform_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'Terraform', :js do it 'displays a table with terraform states' do expect(page).to have_selector( - '[data-testid="terraform-states-table-name"]', + "[data-testid='terraform-states-table-name']", count: project.terraform_states.size ) end @@ -64,7 +64,7 @@ RSpec.describe 'Terraform', :js do expect(page).to have_content(additional_state.name) find("[data-testid='terraform-state-actions-#{additional_state.name}']").click - find('[data-testid="terraform-state-remove"]').click + find("[data-testid='terraform-state-remove']").click fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name click_button 'Remove' @@ -72,6 +72,21 @@ RSpec.describe 'Terraform', :js do expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound end end + + context 'when clicking on copy Terraform init command' do + it 'shows the modal with the init command' do + visit project_terraform_index_path(project) + + expect(page).to have_content(terraform_state.name) + + page.within("[data-testid='terraform-state-actions-#{terraform_state.name}']") do + click_button class: 'gl-dropdown-toggle' + click_button 'Copy Terraform init command' + end + + expect(page).to have_content("To get access to this terraform state from your local computer, run the following command at the command line.") + end + end end end @@ -87,11 +102,11 @@ RSpec.describe 'Terraform', :js do context 'when user visits the index page' do it 'displays a table without an action dropdown', :aggregate_failures do expect(page).to have_selector( - '[data-testid="terraform-states-table-name"]', + "[data-testid='terraform-states-table-name']", count: project.terraform_states.size ) - expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]') + expect(page).not_to have_selector("[data-testid*='terraform-state-actions']") end end end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index 3238f6744f7..0d797b7923c 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -38,6 +38,12 @@ RSpec.describe GroupMembersFinder, '#execute' do } end + it 'raises an error if a non-supported relation type is used' do + expect do + described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type]) + end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants.") + end + using RSpec::Parameterized::TableSyntax where(:subject_relations, :subject_group, :expected_members) do diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js new file mode 100644 index 00000000000..0f85ab582bd --- /dev/null +++ b/spec/frontend/editor/utils_spec.js @@ -0,0 +1,84 @@ +import { editor as monacoEditor } from 'monaco-editor'; +import * as utils from '~/editor/utils'; +import { DEFAULT_THEME } from '~/ide/lib/themes'; + +describe('Source Editor utils', () => { + let el; + + const stubUserColorScheme = (value) => { + if (window.gon == null) { + window.gon = {}; + } + window.gon.user_color_scheme = value; + }; + + describe('clearDomElement', () => { + beforeEach(() => { + setFixtures('
Foo
'); + el = document.getElementById('foo'); + }); + + it('removes all child nodes from an element', () => { + expect(el.children.length).toBe(1); + utils.clearDomElement(el); + expect(el.children.length).toBe(0); + }); + }); + + describe('setupEditorTheme', () => { + beforeEach(() => { + jest.spyOn(monacoEditor, 'defineTheme').mockImplementation(); + jest.spyOn(monacoEditor, 'setTheme').mockImplementation(); + }); + + it.each` + themeName | expectedThemeName + ${'solarized-light'} | ${'solarized-light'} + ${DEFAULT_THEME} | ${DEFAULT_THEME} + ${'non-existent'} | ${DEFAULT_THEME} + `( + 'sets the $expectedThemeName theme when $themeName is set in the user preference', + ({ themeName, expectedThemeName }) => { + stubUserColorScheme(themeName); + utils.setupEditorTheme(); + + expect(monacoEditor.setTheme).toHaveBeenCalledWith(expectedThemeName); + }, + ); + }); + + describe('getBlobLanguage', () => { + it.each` + path | expectedLanguage + ${'foo.js'} | ${'javascript'} + ${'foo.js.rb'} | ${'ruby'} + ${'foo.bar'} | ${'plaintext'} + `( + 'sets the $expectedThemeName theme when $themeName is set in the user preference', + ({ path, expectedLanguage }) => { + const language = utils.getBlobLanguage(path); + + expect(language).toEqual(expectedLanguage); + }, + ); + }); + + describe('setupCodeSnipet', () => { + beforeEach(() => { + jest.spyOn(monacoEditor, 'colorizeElement').mockImplementation(); + jest.spyOn(monacoEditor, 'setTheme').mockImplementation(); + setFixtures('
');
+      el = document.getElementById('foo');
+    });
+
+    it('colorizes the element and applies the preference theme', () => {
+      expect(monacoEditor.colorizeElement).not.toHaveBeenCalled();
+      expect(monacoEditor.setTheme).not.toHaveBeenCalled();
+
+      utils.setupCodeSnippet(el);
+
+      expect(monacoEditor.colorizeElement).toHaveBeenCalledWith(el);
+      expect(monacoEditor.setTheme).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
index 39469bf4fd0..f83df7b11f4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
@@ -10,13 +10,15 @@ exports[`DependencyRow renders full dependency 1`] = `
     
-      Test.Dependency
+      Ninject.Extensions.Factory
     
      
     
-      (.NETStandard2.0)
+      
+      (.NETCoreApp3.1)
+    
     
   
    
@@ -27,7 +29,7 @@ exports[`DependencyRow renders full dependency 1`] = `
     
-      2.3.7
+      3.3.2
     
   
 
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
index 1bd2058cf5b..5119512564f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState } from '@gitlab/ui';
+import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
 import { createLocalVue } from '@vue/test-utils';
 import { nextTick } from 'vue';
 import VueApollo from 'vue-apollo';
@@ -10,6 +10,7 @@ import createFlash from '~/flash';
 
 import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
 import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
 import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
 import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
 import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
@@ -21,6 +22,7 @@ import {
   PACKAGE_TYPE_COMPOSER,
   DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
   DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+  PACKAGE_TYPE_NUGET,
 } from '~/packages_and_registries/package_registry/constants';
 
 import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
@@ -30,6 +32,7 @@ import {
   packageDetailsQuery,
   packageData,
   packageVersions,
+  dependencyLinks,
   emptyPackageDetailsQuery,
   packageDestroyMutation,
   packageDestroyMutationError,
@@ -85,6 +88,8 @@ describe('PackagesApp', () => {
             show: jest.fn(),
           },
         },
+        GlTabs,
+        GlTab,
       },
     });
   }
@@ -100,6 +105,9 @@ describe('PackagesApp', () => {
   const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
   const findVersionRows = () => wrapper.findAllComponents(VersionRow);
   const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+  const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
+  const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
+  const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
 
   afterEach(() => {
     wrapper.destroy();
@@ -401,4 +409,43 @@ describe('PackagesApp', () => {
       expect(noVersionsMessage().exists()).toBe(true);
     });
   });
+  describe('dependency links', () => {
+    it('does not show the dependency links for a non nuget package', async () => {
+      createComponent();
+
+      expect(findDependenciesCountBadge().exists()).toBe(false);
+    });
+
+    it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => {
+      createComponent({
+        resolver: jest.fn().mockResolvedValue(
+          packageDetailsQuery({
+            packageType: PACKAGE_TYPE_NUGET,
+            dependencyLinks: { nodes: [] },
+          }),
+        ),
+      });
+
+      await waitForPromises();
+
+      expect(findDependenciesCountBadge().exists()).toBe(true);
+      expect(findDependenciesCountBadge().text()).toBe('0');
+      expect(findNoDependenciesMessage().exists()).toBe(true);
+    });
+
+    it('renders the correct number of dependency rows for a nuget package', async () => {
+      createComponent({
+        resolver: jest.fn().mockResolvedValue(
+          packageDetailsQuery({
+            packageType: PACKAGE_TYPE_NUGET,
+          }),
+        ),
+      });
+      await waitForPromises();
+
+      expect(findDependenciesCountBadge().exists()).toBe(true);
+      expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString());
+      expect(findDependencyRows()).toHaveLength(dependencyLinks().length);
+    });
+  });
 });
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
index de561bdf8c9..9aed5b90c73 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
@@ -1,22 +1,23 @@
-import { shallowMount } from '@vue/test-utils';
-import { dependencyLinks } from 'jest/packages/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
+import { dependencyLinks } from '../../mock_data';
 
 describe('DependencyRow', () => {
   let wrapper;
 
-  const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
+  const [fullDependencyLink] = dependencyLinks();
+  const { dependency, metadata } = fullDependencyLink;
 
-  function createComponent({ dependencyLink = fullLink } = {}) {
-    wrapper = shallowMount(DependencyRow, {
+  function createComponent(dependencyLink = fullDependencyLink) {
+    wrapper = shallowMountExtended(DependencyRow, {
       propsData: {
-        dependency: dependencyLink,
+        dependencyLink,
       },
     });
   }
 
-  const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
-  const dependencyFramework = () => wrapper.find('[data-testid="target-framework"]');
+  const dependencyVersion = () => wrapper.findByTestId('version-pattern');
+  const dependencyFramework = () => wrapper.findByTestId('target-framework');
 
   afterEach(() => {
     wrapper.destroy();
@@ -32,7 +33,10 @@ describe('DependencyRow', () => {
 
   describe('version', () => {
     it('does not render any version information when not supplied', () => {
-      createComponent({ dependencyLink: withoutVersion });
+      createComponent({
+        ...fullDependencyLink,
+        dependency: { ...dependency, versionPattern: undefined },
+      });
 
       expect(dependencyVersion().exists()).toBe(false);
     });
@@ -41,13 +45,16 @@ describe('DependencyRow', () => {
       createComponent();
 
       expect(dependencyVersion().exists()).toBe(true);
-      expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
+      expect(dependencyVersion().text()).toBe(dependency.versionPattern);
     });
   });
 
   describe('target framework', () => {
     it('does not render any framework information when not supplied', () => {
-      createComponent({ dependencyLink: withoutFramework });
+      createComponent({
+        ...fullDependencyLink,
+        metadata: { ...metadata, targetFramework: undefined },
+      });
 
       expect(dependencyFramework().exists()).toBe(false);
     });
@@ -56,7 +63,7 @@ describe('DependencyRow', () => {
       createComponent();
 
       expect(dependencyFramework().exists()).toBe(true);
-      expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
+      expect(dependencyFramework().text()).toBe(`(${metadata.targetFramework})`);
     });
   });
 });
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 9e5457aa0fe..98ff29ef728 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -51,6 +51,41 @@ export const packageFiles = () => [
   },
 ];
 
+export const dependencyLinks = () => [
+  {
+    dependencyType: 'DEPENDENCIES',
+    id: 'gid://gitlab/Packages::DependencyLink/77',
+    __typename: 'PackageDependencyLink',
+    dependency: {
+      id: 'gid://gitlab/Packages::Dependency/3',
+      name: 'Ninject.Extensions.Factory',
+      versionPattern: '3.3.2',
+      __typename: 'PackageDependency',
+    },
+    metadata: {
+      id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/77',
+      targetFramework: '.NETCoreApp3.1',
+      __typename: 'NugetDependencyLinkMetadata',
+    },
+  },
+  {
+    dependencyType: 'DEPENDENCIES',
+    id: 'gid://gitlab/Packages::DependencyLink/78',
+    __typename: 'PackageDependencyLink',
+    dependency: {
+      id: 'gid://gitlab/Packages::Dependency/4',
+      name: 'Ninject.Extensions.Factory',
+      versionPattern: '3.3.2',
+      __typename: 'PackageDependency',
+    },
+    metadata: {
+      id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/78',
+      targetFramework: '.NETCoreApp3.1',
+      __typename: 'NugetDependencyLinkMetadata',
+    },
+  },
+];
+
 export const packageVersions = () => [
   {
     createdAt: '2021-08-10T09:33:54Z',
@@ -145,6 +180,9 @@ export const packageDetailsQuery = (extendPackage) => ({
         nodes: packageVersions(),
         __typename: 'PackageConnection',
       },
+      dependencyLinks: {
+        nodes: dependencyLinks(),
+      },
       __typename: 'PackageDetailsType',
       ...extendPackage,
     },
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
new file mode 100644
index 00000000000..dbdff899bac
--- /dev/null
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -0,0 +1,79 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import InitCommandModal from '~/terraform/components/init_command_modal.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+const accessTokensPath = '/path/to/access-tokens-page';
+const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
+const username = 'username';
+const modalId = 'fake-modal-id';
+const stateName = 'production';
+const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=
+terraform init \\
+    -backend-config="address=${terraformApiUrl}/${stateName}" \\
+    -backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
+    -backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
+    -backend-config="username=${username}" \\
+    -backend-config="password=$GITLAB_ACCESS_TOKEN" \\
+    -backend-config="lock_method=POST" \\
+    -backend-config="unlock_method=DELETE" \\
+    -backend-config="retry_wait_min=5"
+    `;
+
+describe('InitCommandModal', () => {
+  let wrapper;
+
+  const propsData = {
+    modalId,
+    stateName,
+  };
+  const provideData = {
+    accessTokensPath,
+    terraformApiUrl,
+    username,
+  };
+
+  const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
+  const findLink = () => wrapper.findComponent(GlLink);
+  const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
+  const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+
+  beforeEach(() => {
+    wrapper = shallowMountExtended(InitCommandModal, {
+      propsData,
+      provide: provideData,
+      stubs: {
+        GlSprintf,
+      },
+    });
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  describe('on rendering', () => {
+    it('renders the explanatory text', () => {
+      expect(findExplanatoryText().text()).toContain('personal access token');
+    });
+
+    it('renders the personal access token link', () => {
+      expect(findLink().attributes('href')).toBe(accessTokensPath);
+    });
+
+    it('renders the init command with the username and state name prepopulated', () => {
+      expect(findInitCommand().text()).toContain(username);
+      expect(findInitCommand().text()).toContain(stateName);
+    });
+
+    it('renders the copyToClipboard button', () => {
+      expect(findCopyButton().exists()).toBe(true);
+    });
+  });
+
+  describe('when copy button is clicked', () => {
+    it('copies init command to clipboard', () => {
+      expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
+    });
+  });
+});
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 61f6e9f0f7b..34e7d597cd8 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -3,6 +3,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
 import VueApollo from 'vue-apollo';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import waitForPromises from 'helpers/wait_for_promises';
+import InitCommandModal from '~/terraform/components/init_command_modal.vue';
 import StateActions from '~/terraform/components/states_table_actions.vue';
 import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
 import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
@@ -73,12 +74,14 @@ describe('StatesTableActions', () => {
     return wrapper.vm.$nextTick();
   };
 
-  const findActionsDropdown = () => wrapper.find(GlDropdown);
+  const findActionsDropdown = () => wrapper.findComponent(GlDropdown);
+  const findCopyBtn = () => wrapper.find('[data-testid="terraform-state-copy-init-command"]');
+  const findCopyModal = () => wrapper.findComponent(InitCommandModal);
   const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
   const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
   const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
   const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
-  const findRemoveModal = () => wrapper.find(GlModal);
+  const findRemoveModal = () => wrapper.findComponent(GlModal);
 
   beforeEach(() => {
     return createComponent();
@@ -125,6 +128,25 @@ describe('StatesTableActions', () => {
     });
   });
 
+  describe('copy command button', () => {
+    it('displays a copy init command button', () => {
+      expect(findCopyBtn().text()).toBe('Copy Terraform init command');
+    });
+
+    describe('when clicking the copy init command button', () => {
+      beforeEach(() => {
+        findCopyBtn().vm.$emit('click');
+
+        return waitForPromises();
+      });
+
+      it('opens the modal', async () => {
+        expect(findCopyModal().exists()).toBe(true);
+        expect(findCopyModal().isVisible()).toBe(true);
+      });
+    });
+  });
+
   describe('download button', () => {
     it('displays a download button', () => {
       expect(findDownloadBtn().text()).toBe('Download JSON');
diff --git a/spec/helpers/projects/terraform_helper_spec.rb b/spec/helpers/projects/terraform_helper_spec.rb
index 8833e23c47d..9c2f009be26 100644
--- a/spec/helpers/projects/terraform_helper_spec.rb
+++ b/spec/helpers/projects/terraform_helper_spec.rb
@@ -22,6 +22,18 @@ RSpec.describe Projects::TerraformHelper do
       expect(subject[:project_path]).to eq(project.full_path)
     end
 
+    it 'includes access token path' do
+      expect(subject[:access_tokens_path]).to eq(profile_personal_access_tokens_path)
+    end
+
+    it 'includes username' do
+      expect(subject[:username]).to eq(current_user.username)
+    end
+
+    it 'includes terraform state api url' do
+      expect(subject[:terraform_api_url]).to eq("#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state")
+    end
+
     it 'indicates the user is a terraform admin' do
       expect(subject[:terraform_admin]).to eq(true)
     end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index 576bab241db..4a74dfcec34 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -160,6 +160,36 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
 
       it_behaves_like 'a listing of new commits'
     end
+
+    context 'with criss-cross merges' do
+      let(:new_commits) do
+        [
+          create_commit(newrev, %w[a1 b1]),
+          create_commit('a1', %w[a2 b2]),
+          create_commit('a2', %w[a3 b3]),
+          create_commit('a3', %w[c]),
+          create_commit('b1', %w[b2 a2]),
+          create_commit('b2', %w[b3 a3]),
+          create_commit('b3', %w[c]),
+          create_commit('c', [])
+        ]
+      end
+
+      let(:expected_commits) do
+        [
+          create_commit(newrev, %w[a1 b1]),
+          create_commit('a1', %w[a2 b2]),
+          create_commit('b1', %w[b2 a2]),
+          create_commit('a2', %w[a3 b3]),
+          create_commit('b2', %w[b3 a3]),
+          create_commit('a3', %w[c]),
+          create_commit('b3', %w[c]),
+          create_commit('c', [])
+        ]
+      end
+
+      it_behaves_like 'a listing of new commits'
+    end
   end
 
   def create_commit(id, parent_ids)
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 09280402e2b..cd1828791c3 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -111,4 +111,35 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
       end
     end
   end
+
+  context 'when a command takes longer than DURATION_ERROR_THRESHOLD' do
+    let(:threshold) { 0.5 }
+
+    before do
+      stub_const("#{described_class}::DURATION_ERROR_THRESHOLD", threshold)
+    end
+
+    context 'when report_on_long_redis_durations is disabled' do
+      it 'does nothing' do
+        stub_feature_flags(report_on_long_redis_durations: false)
+
+        expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+        Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+      end
+    end
+
+    context 'when report_on_long_redis_durations is enabled' do
+      it 'tracks an exception and continues' do
+        expect(Gitlab::ErrorTracking)
+          .to receive(:track_exception)
+                .with(an_instance_of(described_class::MysteryRedisDurationError),
+                      command: 'mget',
+                      duration: be > threshold,
+                      timestamp: a_string_matching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{5}/))
+
+        Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
+      end
+    end
+  end
 end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3e16de44cea..0f6ba0c67b1 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3743,7 +3743,21 @@ RSpec.describe Ci::Build do
       context 'when artifacts of depended job has been expired' do
         let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
 
-        it { expect(job).not_to have_valid_build_dependencies }
+        context 'when pipeline is not locked' do
+          before do
+            build.pipeline.unlocked!
+          end
+
+          it { expect(job).not_to have_valid_build_dependencies }
+        end
+
+        context 'when pipeline is locked' do
+          before do
+            build.pipeline.artifacts_locked!
+          end
+
+          it { expect(job).to have_valid_build_dependencies }
+        end
       end
 
       context 'when artifacts of depended job has been erased' do
@@ -4763,8 +4777,24 @@ RSpec.describe Ci::Build do
     let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
     let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
 
-    it 'returns invalid dependencies' do
-      expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+    context 'when pipeline is locked' do
+      before do
+        build.pipeline.unlocked!
+      end
+
+      it 'returns invalid dependencies when expired' do
+        expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
+      end
+    end
+
+    context 'when pipeline is not locked' do
+      before do
+        build.pipeline.artifacts_locked!
+      end
+
+      it 'returns no invalid dependencies when expired' do
+        expect(job.invalid_dependencies).to eq([])
+      end
     end
   end
 
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5b045814891..7e1673a5299 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Project, factory_default: :keep do
   include ProjectForksHelper
   include GitHelpers
   include ExternalAuthorizationServiceHelpers
-  include ReloadHelpers
   using RSpec::Parameterized::TableSyntax
 
   let_it_be(:namespace) { create_default(:namespace).freeze }
@@ -3022,72 +3021,31 @@ RSpec.describe Project, factory_default: :keep do
     end
   end
 
-  shared_context 'project with ancestors' do
+  describe '#ancestors_upto' do
     let_it_be(:parent) { create(:group) }
     let_it_be(:child) { create(:group, parent: parent) }
     let_it_be(:child2) { create(:group, parent: child) }
     let_it_be(:project) { create(:project, namespace: child2) }
-  end
 
-  shared_examples '#ancestors' do
-    before do
-      reload_models(parent, child, child2)
+    it 'returns all ancestors when no namespace is given' do
+      expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
     end
 
-    it 'returns all ancestors' do
-      expect(project.ancestors).to contain_exactly(child2, child, parent)
-    end
-
-    describe 'with hierarchy_order' do
-      it 'returns ancestors ordered by descending hierarchy' do
-        expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
-      end
-    end
-  end
-
-  describe '#ancestors' do
-    include_context 'project with ancestors'
-
-    include_examples '#ancestors'
-  end
-
-  describe '#ancestors_upto' do
-    include_context 'project with ancestors'
-
-    include_examples '#ancestors'
-
     it 'includes ancestors upto but excluding the given ancestor' do
       expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
     end
 
     describe 'with hierarchy_order' do
+      it 'returns ancestors ordered by descending hierarchy' do
+        expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
+      end
+
       it 'can be used with upto option' do
         expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
       end
     end
   end
 
-  describe '#ancestors' do
-    let_it_be(:parent) { create(:group) }
-    let_it_be(:child) { create(:group, parent: parent) }
-    let_it_be(:child2) { create(:group, parent: child) }
-    let_it_be(:project) { create(:project, namespace: child2) }
-
-    before do
-      reload_models(parent, child, child2)
-    end
-
-    it 'returns all ancestors' do
-      expect(project.ancestors).to contain_exactly(child2, child, parent)
-    end
-
-    describe 'with hierarchy_order' do
-      it 'returns ancestors ordered by descending hierarchy' do
-        expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
-      end
-    end
-  end
-
   describe '#root_ancestor' do
     let(:project) { create(:project) }
 
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 4a58f341658..8a63715ed86 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -133,6 +133,7 @@ RSpec.describe BuildDetailsEntity do
       let(:message) { subject[:callout_message] }
 
       before do
+        build.pipeline.unlocked!
         build.drop!(:missing_dependency_failure)
       end
 
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 1c6a25c93ea..2f37d0ea42d 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
 module Ci
   RSpec.describe RegisterJobService do
     let_it_be(:group) { create(:group) }
-    let_it_be(:project, reload: true) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
-    let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+    let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
+    let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
 
     let!(:shared_runner) { create(:ci_runner, :instance) }
     let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
@@ -467,13 +467,27 @@ module Ci
             context 'when depended job has not been completed yet' do
               let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
 
-              it { expect(subject).to eq(pending_job) }
+              it { is_expected.to eq(pending_job) }
             end
 
             context 'when artifacts of depended job has been expired' do
               let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
 
-              it_behaves_like 'not pick'
+              context 'when the pipeline is locked' do
+                before do
+                  pipeline.artifacts_locked!
+                end
+
+                it { is_expected.to eq(pending_job) }
+              end
+
+              context 'when the pipeline is unlocked' do
+                before do
+                  pipeline.unlocked!
+                end
+
+                it_behaves_like 'not pick'
+              end
             end
 
             context 'when artifacts of depended job has been erased' do
@@ -490,8 +504,12 @@ module Ci
               let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
 
               before do
-                allow_any_instance_of(Ci::Build).to receive(:drop!)
-                  .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+                pipeline.unlocked!
+
+                allow_next_instance_of(Ci::Build) do |build|
+                  expect(build).to receive(:drop!)
+                    .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+                end
               end
 
               it 'does not drop nor pick' do
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 4f6092c6fb6..96be30b9f1f 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -34,10 +34,8 @@ RSpec.shared_examples 'package details link' do |property|
 
     expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name)
 
-    page.within(%Q([name="#{package.name}"])) do
-      expect(page).to have_content('Installation')
-      expect(page).to have_content('Registry setup')
-    end
+    expect(page).to have_content('Installation')
+    expect(page).to have_content('Registry setup')
   end
 end