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 @@ + + + + + + + {{ content }} + + + + + + {{ + getModalInfoCopyStr() + }} + + + + 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
+ + + {{ content }} + + +
{{ + getModalInfoCopyStr() + }}