From dc250651ab26bf7bce9205d5fa4a45dd58e2df81 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 12 Aug 2021 15:09:58 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../javascripts/editor/source_editor.js | 29 +----- app/assets/javascripts/editor/utils.js | 25 ++++- .../details/additional_metadata.vue | 6 +- .../components/details/app.vue | 33 ++----- .../components/details/dependency_row.vue | 17 ++-- .../queries/get_package_details.query.graphql | 19 +++- .../components/init_command_modal.vue | 86 ++++++++++++++++++ .../components/states_table_actions.vue | 28 ++++++ app/assets/javascripts/terraform/index.js | 7 +- app/assets/stylesheets/themes/_dark.scss | 15 +++ app/finders/group_members_finder.rb | 9 ++ app/helpers/projects/terraform_helper.rb | 5 +- app/models/ci/build.rb | 2 +- app/models/integration.rb | 2 +- app/models/namespaces/traversal/linear.rb | 4 - app/models/project.rb | 4 +- .../projects/packages/packages/show.html.haml | 2 +- ...ending_builds_maintain_ci_minutes_data.yml | 2 +- ...ng_builds_maintain_shared_runners_data.yml | 4 +- ...denormalize_shared_runners_information.yml | 4 +- .../instance_level_integration_overrides.yml | 2 +- .../jira_issue_details_edit_labels.yml | 2 +- .../jira_issue_details_edit_status.yml | 2 +- .../development/package_details_apollo.yml | 2 +- .../report_on_long_redis_durations.yml | 8 ++ .../development/web_hooks_disable_failed.yml | 2 +- .../ops/api_kaminari_count_with_limit.yml | 2 +- .../img/terraform_list_view_actions_v13_8.png | Bin 36949 -> 0 bytes doc/user/infrastructure/terraform_state.md | 16 +++- lib/api/entities/ci/job_request/dependency.rb | 2 +- lib/gitlab/checks/changes_access.rb | 20 +++- .../instrumentation/redis_interceptor.rb | 20 ++++ locale/gitlab.pot | 11 ++- spec/features/groups/packages_spec.rb | 2 + spec/features/projects/packages_spec.rb | 2 + spec/features/projects/terraform_spec.rb | 23 ++++- spec/finders/group_members_finder_spec.rb | 6 ++ spec/frontend/editor/utils_spec.js | 84 +++++++++++++++++ .../__snapshots__/dependency_row_spec.js.snap | 8 +- .../components/details/app_spec.js | 49 +++++++++- .../components/details/dependency_row_spec.js | 31 ++++--- .../package_registry/mock_data.js | 38 ++++++++ .../components/init_command_modal_spec.js | 79 ++++++++++++++++ .../components/states_table_actions_spec.js | 26 +++++- .../helpers/projects/terraform_helper_spec.rb | 12 +++ spec/lib/gitlab/checks/changes_access_spec.rb | 30 ++++++ .../instrumentation/redis_interceptor_spec.rb | 31 +++++++ spec/models/ci/build_spec.rb | 36 +++++++- spec/models/project_spec.rb | 56 ++---------- spec/serializers/build_details_entity_spec.rb | 1 + spec/services/ci/register_job_service_spec.rb | 30 ++++-- .../features/packages_shared_examples.rb | 6 +- 52 files changed, 762 insertions(+), 180 deletions(-) create mode 100644 app/assets/javascripts/terraform/components/init_command_modal.vue create mode 100644 config/feature_flags/development/report_on_long_redis_durations.yml delete mode 100644 doc/user/infrastructure/img/terraform_list_view_actions_v13_8.png create mode 100644 spec/frontend/editor/utils_spec.js create mode 100644 spec/frontend/terraform/components/init_command_modal_spec.js 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 7d619b6ad7ec06600d5b1e704daea23d3c2d14da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36949 zcmbTd19YX`vMwBU(s3spbZpzUZQHhOqhs5)osMnWS+SkmeBa*Z?t^{q82`VBBe1B=NMUVXtG%@kD~Riz}^4Q#Ba^$czF zji_C%Y`;bW0dcyre>JU)9QANrtt_n_*j>5s{~p2q)&5IOgOB_75Jw9xd{rq~Tmc(< zBV1-`T54K+ZYW$_TuysKV|IB#;eW!vdR+LXj*hnMG&C+QF4Qgz)He1eG<0liY&5j= zH1zaTUn8g-+^ik-T&b)b2>#*ZKl}(9IT+ZR**copSmXZXS5M!@$&m{m|1YBd`};?n zMy_W6VzPGlXIftg()?wip`)gy`FAi!GvogS_Lt=!u)p*A2OZ~M&e&zmT#YQ%1kJ3B ztR22W<7Q%_=lq-Ie^~w%=s%E34o3C@HdbFiM{WfJd!sK0rZ$fM1pjB_e+T|esroNU zI$GNQ!ug*}{|Wk64(u}aW?yuAe@!Ac9VgBI(Ea!E@7sVH=Hb#;AxZES38Vq)_4 z_SW6qeR+BL`T6CMf}pP!%4&(HPs_3!TPEG#VU z@9!TU9~l@JsHmtWCnrx&Ps_{8EiEm_$H%R$ttTcXrlzKjj*j&7^p=;G7Zw)w_Vywo zBCM>e7#SIhi;K(3%J%p7BO@c%*4AunY)(#2=H}*{oScr2k5^V!4h|0N?d`|L#{B&J z78e(rnwka%27G*crl+U-`}+q62Rl1EM@L78hK6QlW*QnA8XFt?`ucWvcPS|;mzI|N z{r&Ci?23wte0_a~hljVewn|G&9UL5-ot?eCy?c9mLqbC4=jXq6k)xwyWo6~a$VgpX z-R$gaMMXt)boAljp{=cLNl8g~c=-1Ac34E-2>ot+&R7}(m{8W0e`&(H7f?w*p8A|N0T8yl;ssVOZjEi5cdK|!IVrNzm~ z$;ZbhDk`d>p~1nyAucX1C@9Fz&d$WdBqk;%BqSsuA)%wABO@cDt*y<)#ig#U&dbXy zB_*Y#q$DpdFDWU>&CRW*rpC(3`gMF!RaKRflT%Sqk(HI@;o(tMRwgGWXJ%$rR8&+@ zP+(zUAt51w$g!^i0>T9n6Xa8LT|HZSwNV^GhPX;)&{vN-%gvXwR%vq)R-d?Ni$lfm zhgI#U3&*RPy`jaA64g?Gf$PO*{2tsd=Ste*izpf@n(I6Er-$a9#AEPnzG5!hbeCu= zhVspDfa#X&XnbAkngZwv9N`HW2p0h4TLKD{2L}|&{$D%)M)i-uJu!msOF&nnyebe+ zg)aBpK(0n_F(r?nJrtmzoIujsz(8^kK!R962u?t}^Z&K;Z&cevB3MA$SYPb_D_-YmJZyFHU|2U!%=rr&$b6z0?NyprF>Cx_ z={SC4LOuF^+9*a0H|)~u3#@3X&PTkcZ-Y9`bO%>~{lBI=5u7dt`2tA|cLPb%)5uov zbq?Sq{3op1U&9^E&FNa7Oq&9UoLt=QZVeDg9D%>L)i*Xhpx8rVbc)V-*vp`nh{R_3~+= zas(STJ;}->?|v~kS>p7J-}Wt8|6NfnDPRM`H^+iXUZu5eq79<5e+{Jo%N^B5)Tv!4 zdi>ouS7og{+&~pADRNrCX46su7`N63QO9C9K7BNEBeI+fOFyKojW;XWfmb1u2YglK_Mh$ zeJ+#kT`!R=#pMd(=L{EF{K4;2<0^g zUJZWcLSB!ZjDYf4bJ?AI?2qn`#~{7RNLu+=xpWnG-bf~8a{+esg#<$g>yyW39b!~I zCHcaeC6%In8I78F?s)L1j5_R4WFPP^ktyxLB)6n@0F%xFsdVD)O8JPjo2zn68)#)V z7lcUU_5M2GF%`#FFSNYyEOE73!hwN3-9pa39wokswg!K1v25Y0t9g0rm?CkzzrlIV zYUq$KYCLL>6UfWDJ9&#tuVPn|y7G39V>I<`ddvFzbOoPEkgFTP<9tDqcg z9M@pia_|fs3iHgj30oahm36kJJrmh04M<$=N2Wm8oKDv`@XfFx zZOb)?f?l+2uv>H+NY6Zpd;4jlIvMTuNc=Tbh$A*Q6njU3-hbMoD2C2ZS)06N5>B9| z_I#qu-n~Wc$x`zc#)8*N79mP6-d^#KtKH5jMJ7NySgbRDHioPNx>523s_qH1wRo6> zJ1MNxLFHnek&GNM4}N&6ct1yEy5}4NEwvYZE0uanM>}p9dThG&3K}EBfwxys5$2*EPEB`Er zY%PXxt#=O-*`%`DZnc>9!XAxqt&@8RGo4CQ^u}{yxWDGKFG?lxm}IKPS|)!DoS2lh^@qCP;`OwO(?eQu*Ww62?Q9-~k}Y8#QNWY&(Sm$KDn zCj60p!#s5-5{S8*xb1v52U^8dOq&5yty#)R{XlE zQ||MP*RK(j%bA+Q`Zj!Py1CuPMG3tano3cvvVh=Vui*{x^1b7#`3QvyCtgm2pHTk( zZB@2($!+RflXKMNAfJjKv)mN{!M&G5J~5xC>z(C?_a>4nQ-V(UpH7bTqqnSt-CFx7 z&m!LEf{iV$6F@L?7+T&&_9Zg{DwmL>*ciG`T@>^#Wj9U38@$KSF2;b}&~;|2fwi+F zi+Z)a(CS7kwFQnSX0$~l76n=en7()@8L+@lNGDt`U&t8rG?1M_MJhRJ58}$7cmGEtkWyANXx& zu2Us1GKE_#y#rzb##?B4HjTZttaFzBE6`zplQtwu>h3{-&$PXaLhq-A)9j5lZjaNu zvv`4dBR9!h_kK7z?VsH6;yzwp4;bXHOG*nyW$yYo8Zlek9eW_~B7b~va14d}9HzO- zY~Ucof#C_D?C~KF&Eg@wdM(H6Wa`#da$iM1;8%o=#|`A&StQ=%dNX?iQ=h~L`z{MA zO073Z;WDmLen~>T;%FlsaX4-fA@^dcHc<+hyHC9&7JmM?1O5?H^;COaC?QbIB7n23~V#( z_b@k}xM6ssj-A3OvF*K|Ll@>hG#lgfu;eo@6DKo)u{iOWID^mmj;dmUjuXFX){JI{wpw?f)j4I*8P z7!h>Fr#II?f2`9mJiFuu6v|~jw4aH*S&TYu@(AlNVXrlo(IUdP1$t>7R9t%( zf_^54{d$%u8AuxGWr>kLOT(h9ryI3-=BsCTz=r0gJ?Kv#0rPSvdEIxG4bMvBJ5sN# z=El5k?&w3J_K%TTA)B%|$FY^v5ZU(HlAdYI{QfCviL>?W=pP{}lN}xo`XOiESVKLp z?(oT;NrtMKfj;}gh=L_k@g8eIuT>R$w{5ssrybQE&)Z^Y^X7L~0CjIhzPJ~c7vVtI z7!h{pNQzeY@V%^{UO_}yeJ!mR*PMP14*l(vqiS?2g!gdi5ycxV`UkYqbDLhPYzFT4 zKHSLg$EvO$-{n@P4Zjbf#`ZwuD?Z7Fcir&EIU@1;!k$H^Gbz@h9-IUMGtgQ zyzwfj&OHLgiSIm)EHY+ZRQOJ}!<6URFIE9!! zb7#q$_!%Qo>?4xeNkH>jP}gFEbGEpnZGiSVYh6PHvLz)(>7l65N$4Lx&iWei4UFhd z*Y+P9lmfG(8cp2r{DhKjCZFUAV|uTgqX373hC_i6XmAf4zGxptDcB{X!SST;Rey4) zijHr~0FjEtJf*+}ljD!`d#E#|5MfyJEb~~tr#Tc)ZfIdR9hAHSH!KMU-j*i4R;dVt zPA4C%10L;AFC5JQv@HY&QsGewrL-o}vOS0H-s4qLB&fj$!E8Kc`ouh)jDn>#S*|Gw zEX-e*<*o+-T7nT)TA!QChE3AeLH13qm>4+e`NU%u%j^@K>`67qi%}3c6sc>R^phz3 zC=5aVr5bVmVA*ROCTokq>_pO>Z1$!WADr8NhKKe?K$Omfm2ultiz8dT@Q1jSn7Ln=(8lBes&=D1a6jcz{KTK%Lj4d@sIE zF>4x(ygSopOyL4`2TM|Zf4OEQzG$j?=rhZsjjK9 z^okeZ&`ZKR{;`mX$`PTYQ_&@6GBH|IDYVXh)5zzfh9xD+@a$ep;Zy8tqq-OC1Xy7m zS&>^-wU|n>HB`W0$>eh{D&~Bg#{mlqzb*Oj{ScHGJ_`zFXjn zaa>TCI-nma?@GCQmoBnX+2Gz;dATpNlFeLcu|%?M^)!rKU}hBC&TRN02mNm`1hhrzzHaa9SEol^nVK7`!We2 z-YMS_I3PsVzg^q~1KfamK7nPifWDNzU$-^?o%nz8ecApux_`#-H=druAx|6NZcPbFh?tygyaTz2Ht$cHMhfd9(l zv=Ob`DKG63guZggT%Bw_Gvu{WWGPXFBsgW8NEM}?cv7~=xsTG+ZZR9U@<0o*SpIU}q;gYM4O-PLkyMtU!4}XYa@$H=`4e11>9f^>z`cmyTup@VN_kCFIcGr|HZgM- zacXJ5K{Iqyf{rVjhf($?K7xVUJ?H8bQxu?#k7V8o8Fu4k??tJt{+?&)V^oq%7*JGa zm85=vELSxej0JcBe*T7)nJr5TcE1_tC9-WXdB1QpXQS0#vT0gDW@1opjxT85q`ppn zR}9gHHG}F@ulOgaRHky$QORmZ@$~c#sU8+>7DJX(ZXN!uq##78t2$q4%7o9-EvUg@ zDeAO2TxWEN1W>h+Z2!SeZAHkQ18o%GwuoL?#=BA(=v|mZNM)T?Q^BX${Kp>hl#9@V zoBTNz2W)Pt_+5JpY;%M0Y4iquGFiMi4bw&NG=Ku~frut)i5~1D=6SSUeniV?H*@gj z<91+2t=T(4=%AB}1ul(=wK{cY*MkjhmO;zu!27;(TQl@IT4ww9)boR}UFy(yGQ3u#CX5RbAqNrkAGmxotZ6a4h{eVahO!|DJz7_(bu*IPp}Or5zc9f@8l7 zL-7DneL*p^D79XVrOg_*y*p0ZPyqVG8O!2vzN~x6{()_> zdH#dKs&`P@bPbQAiE4u-*;~NJh@8(^mzpcaa=+WYYoQf_oWl5Fp&Q}lU^GT?1tC+T zRes@m+?;MMopmL(1n{T`)lCc1K=}?tqkP|izLRE=>%x=my`oNhHAH}QFJ#M8phR4=2%Ir*((!DprQqnI?B60TaLtr#`w{6xH_45G|g zE2%Qv-!nuW4BQ?9!jusN{0Ur6E+MCw#NGZVxg@Zxyj& z*}H|KlD#NF%f!9S!+rC}i~F*3aE?49+!w7EHDvHR z&S>Y!aaT(c1&RuuYaoqdUf2zicBHspV;q~+EiGT=+v0^T1?$$clpn==Im_9e4}J6k z&l8PvzMPVvYxO@h*FWZY=f4Sw%fRJ6pig)RcTm;-2}daFGX3yYV*l$j%8*4l0}LALt5%Q_EamQ zDN5kmVHLstBlYX;mRaw4(iMemHe|g=;IKGfmvVZm<#4ZU3UuO@>`=5*lsDF^utLeQ zpKz|VeRjamQj>fe5yt2OYbTC0JJ#6H7>>>OO?XQ|3y6Fj{+mhOKHa=q9V~AF#!lI& zl3S>_<>C)&aw~O622Npv(Yytc?Z;-%%Y6kMo%v{F!;Wc_1%UEvoa8hTDn4UXV1&-o&}3O?do+J+`+ad5M^; z(oRgH2ct#df_#Rv3yYIn+DXzoQ&*$Eh>#d}hXloeLn#zh#nN(Pyu>p~)SUA)Q>Gky!jWOrASeG80cF?THhI2%i^qbRj zw96fl0Y5Q7wg#m-T<_zb-(4Grn_0b}0o?~FG{>Eph@1U&4G!px!>t3@h*$nG7;JBI z*19*zM%*ogO*Vldal=wwYG`c zgY?}?ClA_BX~0{G7CwYic5)`eO(RW5sRO!7A{VvQ0?9;;r#i;K-3oj1aK#p6B6SL~ z^^Jgkg2)R*eyaXEQhHpD#!!;4&ypQ%pYk<`WI%m=-G(^1Amcb!AnDsxf#^_~p72c=C->*Q9^>lS+EACc~WX0*lkjHsfgA7qV&%fRvtfjX09 z--{xTUi@G*%dXIbGZ-SxN|i8rPyP9Zd>>O9Vf3*6>~+(g?fk}xKg-Cd{dwktZhj}c z$xYC`1XDXO|68gmY$$=s#=!oV2sH1eT&>?3l9(~X^|vt)6vR^JaQ@Z~>oFm}5;8({ zF-DZKtJbF*!+hnVt(^l${89#b4z$gxg6k8&bgK% zO2#^Sr1IqIQfBqK{Nu!*RV~~X&iL(+hXh6Znn;Bh$zcZ(0<2O<=J&F-$v@?hb!II3 z1Tt4fN304Dyys#SOfx@NYXlaVz~`PVxBBKqBxVujK`p|9@|7T7e<$rM3@2wBDT$`K zK|A3+;2FLQd2lEr^HStph6U^Qoc`MHUg`2#yNu+F9aVy3VC7L=h(NQPkDM6C{_qIr zQNFpE8HLy)AjP(&V{3srJl3_r^8e0Tg^lUSj@lPf5ZyL?xR4(};@ofMVi)ejG2Lu_ z57WIN5`xF#&GSGDZ5y+GmIC6ow>|%7%uSGU!yae*DRtz*1Lh~U{K0nXZ>f9byqiio z`^T(jXpCd0Z;&%Xd|6veOm^|u&MFzWS4d7MSJj1_$1CdC6NZ8#KQKV)V2?iqDQXP- zWyEzK(@=2yq0+<3n*GKnv03i|z?gpXc79YD4w1si=X|nXUH8M3d>26kdxT-1vLIpC z@CG~1%gq)|?A(~SLsx;0o->rynV@Wl;pA(iZ31tu)L&~qI+oWVxmhXJocHUnncCEA ztBovrgCu$cVQvN0O}efJ`Mxg%H*h89n{4{V1jF4srzJUQN#dcsU@HPyi-$+h4^|S< zFCDL@O7hb9AO^u6SpO?sD~$n?`VCVq80uO-IS?)#gp$@R7Cqsy_Dv{57d4=3QHN@W ztZ|KQg&s{XLRI9WJeR0DJQQQ_R&o?PZO(hOKak1=1Y=@aKue)5e1DOV7Q^5kxBXFG z$D^Q&pI@Wal8s5QxP}ZAX9>E673@YxR)5BQLfe34j2w(LfU1gud0ePD5M4Z*D)`&g zZJgr%gy%sMaivAV0S_gDlLd%lDHUel<&9_kWVh;!NC^qrC#qawTG~v`MUwXG& z)@n1$djHk3ENngSlu<9VSENs!1CGb!)t+sS(INvQzS zPXVrVxz$JkJe%Li5@;XUUA3Px7C?Xz0Zo2f(o zYCdUgu#nrlIC-~wQ)M3tWQx!+Z&>8eMdtITuYbZ!M_I#lZVTDL4pmR*)Stavx@UM- z14}|l7hmkWl|^4ddst~Yx`hn_z$)qV{IuOqH#V z_{?&vuQR2--)yQIY~=Hbq{4~@g7tg6{lgL4FNnAhr1Zmi)V9HXd3jZ#HKeZYfHIG^ z5=~6<(Y)d;d}?&`kCCwoYhlOB+g^ylWnJgC_OHjtS)TM1*_7PF6_eiAaATsaKam@u z>>+O}HZRQEcU6;Z8o=+_p0&ZZZvauc9DAN_OkelJ+hJs{?$O{iNOB_=nxe&q07D|W!)CbOS`Xy6##5hHZpzITSw1*v#JJI#!r zR}*VCkkKcbEpzi-5t#+39qa3y$kZuFx zC`_{r1ObS;kLYVv%xJzvS>WC)S&?}T@<5GRmd5wi(O%WhD5ey88(KQ(FVFs@D8C&h zK!c%g@F+}b7rgyUEJQUz9?)RM%S!%ak^7br7R_}EO=j#A2ntuzi~yC9z6_jN2*)Wn zM=5rffb-f0*W{Wth2ksl=Ckx9eZ5O?ko$qROEpD<)5Wx+32h0>1L=%TR6R+hC1f6D zo3P#D+HI=gWL_%RuOOg`@hdz(hmBELMOGiUIV&+1xm^ylvzGYKKo^siC0W|5VzG|# zL0o(C`$c0%5gnXLU*te746p4pjmUA_giFgr)@LukVsNpmPJ zlN(lxzrR`DzB$iEc&N9i9&_|Xn|3yPQk_7QJK2v#JP?mCSMK90mug77G4$XP!dc^4 zxfIPyM={iH-YV9-f6M*@_oiNqn>p=XZ?K0%7#U~cw9^Zwqjyrudi9M0ZM^j3@I0;9 z@vuq4d=FLmGL(otc@o2dH7OVYCv4#tZ*UtTNWRqQ$;q2r&Q`zQP&L#jqB&_rRE{%a z4Qymgv0+^8MJ1Mi&V=tX^=w?3$2!-?+C&Q8#3kqiG~0P5EAiV%4jsY`@W=L#ZtMUx z6GhL@kL;WaRTYx-O<$>xmmL=pZi8uwM#>;C{JnMZ_m35-!B_zj8x)AZMcD zzaQ?lT56Iny<9n_WJEbo$3fcsO`x^P`W-g}az(R>BYDjEs18GkzNfy{_HjBpC(9G! zQ0zG`X(OU(Lr=g_W#W&KPUoQ5C6aB+p%zoGDYeRy0>ipT$wh*Eaa8HAsu4JwG3O;m z8%6Ts8BUg(_W}i6aRDB9OoA+O7sQz=hqpZj$ccbh*p)^G$Wu-yxd_-Fv9n$TgZ8l7 zhLq}-3_JHquoLtoYjrAV=TMurqZ84|q7Ec7AM*|^#XogfM9@tlrP$PO(fPJ?tvw4% zr&g#RB*xw#NqSI^)H2sWva4a3uV>Vt;;O8*V0yCDKn)PPf1juazNnFK%c+faIAEW; z*WeT^2ookVnG1T65SJnZAO^GNyrea57BIwimN<+eFEngmM+dip2cEY9+q_s$Q#j&o zoQeQk;5lL31+Qjuqd2+{c)chIT%>!okp^GpgKcuNS|F}SQ>%+3%Evphr+;QFT26iY z9+7?z_Q{;(hjXis#@!5E%Uz~ow<%WV)b5qw3RCilViQJWf&Pje*lSjtWOtrbcn7EQ z^qD4^UCiF=D7?}ohggyvLcHG;_jXwei~jaCEjRjHKZLv}n5r*Ik`3kyC`hi~N*fL# zU6Mu?CLZSP3ciV*BHXjt9e4QAvy8=z=sRutiAV~zi-2(LW1;VnAZ-a=GxK+~oow(@2N)-lF25UBZD=!22Rw(+|K~VsWwfhYS{fStG z{$xtJY`>e|aJ~^iQh4$Tex`0+!~#kykPBy;!E7&Z<|cqAXJjR@V0E@G=G9GWZYsJm z-lQw&#f0%Od@qfihk8(NNS9!x8}ZzrJcyCs^|uW>+Od|Lkhs{p@&no6Oh-)8%@gri zmSE)!Mh573 z)vl*ny_Jr5?mIG|$KeqW%I#t2Ho`|c9=f!gk4jaqVaklXu;2z{5Gw}(~9O@u4Za~TtW|MZ?^|DCEZ>a2e{$-dwZ0@YdphrV!;EGohn&aXTRBTVqZJTuq0xq(Hdrtu|f14d+=~)K+d4)bKE01 zKG|RGTgt=01>ViI*yNj&FwJ8M#c zFW&r>`d)6iU44&!M_tJIGb5h8kDT>VeKeaBK(Sue>{hY^5bVGZTDI;|DGyF==J}VrrTcptp(dip(Q{&W4m(ELUTM=xpo)0U7PPBt9vItP~#PVqc zC<(t23$4MG$xR`ne~>l+-}sY*fRm?*3YDS<6*ZKl$(EM#D-;4`Vpl(wKZ|eMnne)9 zRO>v?yt?t&w>G*qE<6w24pUQ2>Y&yuMLD8%;1yNHo z(DtewDq~uY9Yks!^%|qr6B|U-hSeGwGRJ=~)Dlhga!&n>QWy7g}uPf%Nub!UPWVx#M*jab7xA zuN`;!rKT<+eIld>T}Q0d{jEl4?b_mbUCsM}Jh48K`}~hdA5UZ~cl`&U z9JH0kS`Yg*FMKSI1(Gq>uF_HG`CB+E%I;||%?Ax2)%)%^j;bOx;zusS<|AZQ&y@2K6HWL%}{ZxY(7*mwL(=}^mQf&n=YO{rAb5ZpPPS+Bw5Bcqy=^Xqec z@foZtC`NZjTJ?I*$ZQ)%8fN*cZ?H6&N=!>uCbFKJ8ss($f0SEKRN!qwlPR(|$Va#u zx``zLF*Icc@i~vrH@c*N?<|~ouZ3O@Wml;V1gL6g)TIxX{ThXuPkC<+m$9=Z0nxYK z-B1_r^prlmT5a59%WJi?+2x%jWf&niTbJK2-`=e^bfG4tp)gd}grwVYVS>RgqgXG7 zyCx#6UA2Ero|6m!Gmq9R=DxSCGat>b@D6nq+lXGoSg&i%DsQk}S6v|qCgKA8YzvtC z5yk4o(Ad?OLXVk-GF23wzIy>g(WKok>{;X-R7Wat!scw&VpY8SW7;kl%Df&G^h#ev zZLis8#vvawYfg1b1$b_Zj}J5E=4?1Nu^FoGYpdFxs3t=%@JBw>*YS%z4aF0gVRF@` zN;J!l{8&=g(X6D~h}G0J*Ac1>2$=~6J4T3!=CGvt;dU6g#tARF4cLK$Xr<~yK+`Yz z60WkMEje!l;gN~oDdr9-$uXc|+d^6=z2tb_UJPy&5r59%{f49)-gsq!qvJ^Q5PLX} z|7hK~7o>0R;_@OU$eO&Qr(3c@Yy!4UW*2DOwOlt(3#}g ze8(prPq&y)leU;Dp*~}|LQZe2za1;-=qzh+!*`&MOdxKEDAKGU4E+#O01??_1kc7W zbUhniO#x#`jSJN(iJf8PDiCkz5z9BO*^!7JOdY^di)6Z~Wf@>f9SJ4iIdm_(8q=n> zfH&dazYh8~;vkoPI%(MlqToVn^$X*)Eyb;jy)e6uyUosO5W4DvMaXX&&6UMlR&F!+@Th zMe+)XL87pgsYvjENVXC>f^4Fkt~XvY=47vySwkg?={MxCqlO=Ct_f1K#>x(@nNw9$ z3aI_HdA96oJUb5lFsihLX}0gj#3HlA5Sbe27&tIeq?jPmCS!-?uzD}Oq50H{RIKSD z5z}?z?#-t=C&PETw6?hGwymbXtx%@IVj#wAmiNR$#K9~SH?z-?-n*<$7jF?ZU=}52 z038oMn^GZ+tcNr^2tgP*OC!u|MTBYlVlgZlfb<4|$-YpFSfbLu7Z=0^n)%5S#$JMOJisKk7pCRU#Bzu=M z&F*)ckk~*^sLU6Qwxp+TESd^?Dx*~VBsyQC`)jD4R9y*PNepv*}?dDvA%>gFc|Q^`nWl8dMS3ebD!cy-5M+P+-HFxog(X_yiL`k_df(y8wWX-F_6k+ zKdd@rfiS$U$d@em?li-H#E=S_iK+K=2fpQYX?3D5VGR(Z-*MS9C)b9`1~Ow$U^2)u`9Q&CeOkfdjnnpZGI%H7&{2iVM1I zI{*^(lfLdA?U$Ta^+Pwn;D8gs%x}*IYCNh&hHxdtBP9?EGZvkyrDTN#5kspURt@MC zvtQ}|{N=Q3;&H>vHBoZpQTIX4vF_`h_Vx%cm9Ys9xNVr7Wa#AT`)is+v|cXnC4==2 z?i{&wuYHIWSg<)|Z5-x30MC);&3nNGm%tGrX(LM>zU;CpB_{4l#q6pwIvQG>%bPH= zB67Oq|aI7f%__8i2yIwSFvP6z6$#f)$wWm zWT?(pDk7Z#q;bmtB?v$bfS2nY+;7CM_75-DMhX0wUv0TK>9_eg!-bum<`7W$^3B5% z2;1$oKYs9Ekw5@=-znrmW^*Ser38??=5GH2P02h;!B_##-pcHOzWC=aO7>8SNGTy8 zx~MQ+0}uWI!}_1${42smASZogSlU8isz;L~7X|Q;2&(yPV}@s}+>8^h0}p9*lKLUt z!_K#?w0y))V3kkPoMyZm-7si{;)Efhhw%C5d6I;|>K9BwoJ;_Lt#$OkYMC@BU^*a| zT6aXb{gi9rf;U$bB#O1xkRG<3Uu+bW&`h}%WZ@?ZewXV-3{I)IuZ3hQmw&K^61hD@ zJ-#Kc%-bmq2J-U=K6=erNp;Lv`K^f!b;b^36?K0M+f#e`SgJ&uf2`?}N8LOw&8A{~ z6Fm^%s{$;slPjI#xvFTNTOr+_$0qgYW?M;kW#{yJ;W+#T!RgUPTRz$?;}}G;*EVPL z1mhP3O}}kgO4OXsD>z4YZ_Hzyn8>M-+z>z4b@KiA^^P`lejI>@Zm?IsIdHl==g7a! z;kjk%HzUb4Ord%3bA-=eX@LX%RrO1k?UT+?RlN%NUl=8VGqHN-oERjUbV+wLjWW`v zdDOp^)5(^e>vy**2&N)SQQMKM8k`Dy8w0RQobZnK_nT`>BpwI;EYF z^D2`*gR`iGmkv&S@0gVF3pSZr--#XJS#IO9BHVn^dW=SU`P#ArX^*W22$TC3=(!y!0s zx<#IcY9CevseuP>>Xs>(NG;TAmHgv5vum-Ws&y>VyAe>ff$NxJBSqe)bA!ZPs@6Zx z;)BHje5d?*VBK_&C6Z`wcKC(|PUU{SR!qckfF2_1A z@9k-n-cRfA*dR6@jAv?6?(1qL+^1-f%z-vX6E(Qh&7?wl}+pWRL z#ABVu0>IttIOcyZM%Te#dY|us z9o;v|>k47@u-MgW86slte46C)!#O=ZXUm$1_=+>B5c&BwzHXL(Mw>=Y2_u~`hhNiA z*aYXc%0*5KXdvaLX}g)8VD-HUvviBAz{J}iHF|fnIvyE4U4t3~e`@o-WckOf5VCv0 z?$+2^tmcv?@3oAZyPXs6aH`^cdkyP9zL!syAtMA*3?&j?dudD2u6UV zJ>*r^*9#T6vJqU&weiAFgEGxr1t3x9*+FR43wAsoF<=km3-?pS2;dD=Y0tT+0~-nA zOh+qztt!+r%UDJTigj+4AQJbmPUqQSEyQc>J!qyPIkvWvjs0RrQSzjYFBQ=N! z${?<`Fnw(3(dW81P&` z%L7TvaO3Tv(w9`B>W#nL;|lswLj@et@~C`8DDvn9G$9A0MNT;CxOI6a$c=$#r@`pJ z#;4E>a`Eihis%&isL;YIW3|1+nrIZ#%?H{^Ax`;NkUBzbO;q_T3v6STt>5rb&h;R^XNVsW<=aC4I>&KE9+n5RB zcSNC84yXNS1zLR$op@;@{Bc1C*9r}G)FJuLqAP576O!tGxC7Uxn8<4{>-;G0Ulwi~ zng!dx*`OUvN1sI`7}pTG(XIM9Pd0H!Xr#Zrwu+H2t3Fxf`=f(`9W=;SJblxKp$WR5 zJRjP0PuBw1U1j^8WGpLvK)f1)N+gvAi?+aD6ZDz|jF0 z#}!?HV6TmvU@;6}5j5MMmh?qtYpuXk-;*XZKIsisu$YUPce~9n;k<|U;4yHVINp$} zDaI7Z#Czk~K`y+#i^Oz3>h!1e!N&KK&80x0Z%)2m&Kj=w7=1`!5NIxVjBrjNFV(*< zZco3yQdZkLgRQZnro%ZUcf$r?32_HBGGBJ06*d3LLG<)uk$aKW)pVgRrf;cH#%XNd*#`mq#(E6`Qw= z8&=7+eAm}G$ZH21^ydlLy&374-w>ser?~Q5r-G^S{&=k4`sPc^lG<0b!b7=je6T-{ z!>dVe!z?4})VUMY7x^OMze=2@lDS1p=vBKr5%>Nmnl|oeyl!8HKMS*KDN1_fJB7AK z9k%ebxqaLU*At*}!Tor98ZtxZ;M;Hsxq+k{2S8X?P zKjf3)r1FTvKe&n}=L@INTgA9_QnGmcAvEgpG3TD{9&-AX9V_x|<82_EiG$OLvk&kW z0GBTPjl+MGJuqa$G&ClfcIuy!+HsGyPxOB?ND8qt>&4|bt8!E@(;m5YvPf$5LV9!ouwDE(zZu@HKR)u4El)pO%igO+9)h<7U1*H+{KsKu&n^}JN{my2m(sjJey@SJ7-{dw)s z$oRIK=H5oC9PDhT@TyT~dt4OVH%o+X0kwE^hjQ58uIsKr12B+8@nk{$^cB&AK>Ucw z@bKjP$@4y7-?!MO+9hu4eM%goc{g9vhVDHl*%+FRFdlR0eqP#~%i3X0t4o|JZfG}j zE0@0+974A?zT6*zOu1z4H@wf6!!NZu1?n$j3N%L3NMYjGt!*(m=fidrUy`f*WX=GY z2fp8ePiW~jFfb_{{Pp;Fs3(#-%o5W+9q48Fr=Qi%DJXOlf6;p(`liK>G3HgGwUef= z)`94QX65$0*fTTZs9oiC7;A0kUBN)2yyp1pezA-%dk$Pl05IS$`R%WUt}i><|Fil3 z2~h(2m%}fL|B(L+!@ExYB$?G_-B$RSTNnb(@J?Y}08GqQBhUJ#{JwYdr!Bc-S$hUf zy=D>a+4WQhy6+tL@BwfhK%|5$9I9q89SWWB0vH=Z(m;-|nw$Gj2}XYpJ;tt_wFjfp zp!Ty1K+4t^P{U{1gi3#1cqsl|I=7&;RhwpHY0&#MI30b$6w%{svl=)PNlBC)$cfjh zFu(0r$UVxzP>Iq}9ndNtm#h>#BKiV{BHHq3j3K|w&RNR9c(iMrgAliX$HUEVfKW_e zwRLEFh!qtfuW2^XomVe6wsbu)zzxQ)s;M6b!6&n|&y8|5K+H4!vXDcRI+&jzg%bcN z60J6krAIj$4^z;IBqiOS)Lv$BB16@%lcET=!RT9qTO{b(K|nW@A;Pl#ve)j_vg*KC zlF5q7-vXd?1OfQIK;>KGwrY*SF)3{4p>jlBDM2e^|8|A+M4=QnR(Lm%y`%dv`5g+G ztWpB6Yv+1#nhx23G{T=Riynp1yQQ0t?h;!+dPg%rAgqQIu^%)AZ}$HX_Rhh*HNnGY zY&*HJabx?&wr$(CZ9BO)wr$(C^NsCf^ZaVxeRr$2cI*6e&YY^Lo}M1m^rw3g?X&D! z0?46~5-3HTAM8?yBODytXkR#A2v z?lS}b5JcPG92v~fcs$Ui`W>q%XvUfwNk+x~fqDSzIrAB|03j2dfaI?t+bewCZSou( z)L#Z&F3(`+`4cL+LO{y$Og;z;9||2lQ|ES`eAsXBn;h!m=+TK(ZXbF|$=Pq3kCALP z-?IEuNLhdig-OJRbOtfeTNhN8?dmJ?KiR6qoMj+@IOKEAOGSJ z#jxLbGvNtt`Bt&N)mxl+Xj9O!KZn^qVdHVal$?jo;l;3Rf=u1KI}6`B^#yTK z%u$Hyf3W~Gu^kbTg^7j~KZC4>1rVu=jvigjPFwBp%jFE76ywfWlde~r1;bs(OCeMl8k!+|(=$3kzlfMB0y5*H8y?8*jhxN8*`_#~Txy6|%g6e{p`&UQ#Tj*W8e1oh@_F-`Z9SAo(9F!+B&@htm~$X-H??@uWdYRKu_% z>YT$R7<9k5*Cjfd1k!mH)Gyn)Y-y=VX`kS!#e;qmsu9KVyMTCFQAE2~z3NE>C?`Wq zmE}Kc&z!gX}F0O@Is+J z>UYTB)bZQXrxsVdPDgf?mYHiMsIl_nEg){{uxSuI+OPC-xPK~-yIU}0aDOkI(|kZN zz1-PiQWjk|Wm4=`B=1>rj1~2UEc2{`XjT*_?LlR4yxWWKcMFM6>Pb`(@0-8XXe-*u z7RtL+@W$jnqXqSn(i7Vo&qLoDB;J%BJ4ZPegSz!6qa~rr9#&ec+%f96o}YE1Td=g@ z#>wd|sTcGiX4qFJv+t*B1%)<%Li zouaY)GNQHT2UbM;-hEB~9_g&LVBAI=gpM%)?Q{y0NvfrJc3sFop56Zd^7x~%H+ATu zJLAq4MLbi8ijKnd?Hz4`q73OyoNs6~?CFKQ6}BJ;Vx@-38^sJIU>g4 z;<39c|81nRq&0cwM`VohE=c}(`KUFFDDEBX8k^g9CwV>_Z{QLp;OlAcn7E*(_^^rB z)kMmuDKz%y+=xo9IkvjQdbIaB?eO`c^Z7eyjo%6=>Stja;tq;-fM>Kk#X*ZI-bJ6e-|$*1#!*hCd-vlprKmuhk!QeXwCz3{z+*Q1W00`wXYc z-6{I_WY7>D}r0R1B84#HVTnNr)#2ROMk z_;%kMg_V`1jixtz`6N0#vzZEIj*%jEB_|-SR-2~%tQ&p^Z-eOKE%8U5E7FM=JDGF` zE2L?ACs6R~F@PAO?GepHk^hJ=mySZHCpKUL+~v%0X@{!Z1kIUR(+)*sheWoO;eoPz z#zPJfh!>5CYS8K5+XX@#@gXv-yx5i*bU|M5oXeLddPOqYJ2XA^$}-Ko0x(z3M809y z^x*vwh>z$ogE@1pg$S>K)-etA?h}xY70Uh8jR3|&8N=N>lq%cmp2(>?Iql== zp;bS&&<^%|H|@TA?-cG*CPUOvwJJh!Z84u^)CQou_T(*6FrHG;5HT7rh3l5Su8zoV zS+$dKI&`J^ z5bJb|J7L^PwAhe0wyJ3{n}qfA<4yEl8mcg7hFz!%i(X2N!Y-H|8dbL`S|0El8LTSI zThh2m@mH{*96{-!fC;k5J@$g!T~5Bq(Alp1xl17n6=k9!efLih_1%rJP@8`4P(X-O z4?OS_)GZnSQBxzqTU4Mb9`6sR(Uf~11olyZywLfvV|^9eqY29{?TOEyxh;Tmx#2H^ z4)I9RU8OW4Oy0kmuU3B+L?;s8sg(n?r?vboY` zR(}F@Jh#}$A9)li#U!qC&Ocl8CC(fc+~5MCopP9gUE1=~+$$eUsWb!S$A`dszyUwU z3$*i^@#`V0<>p=q?A($COTA4MdNTOS2_SEChl8<%878Fb(10RABpZVkt}CP^%@}m2 z3$HY+8%1SYRfZELd)3I5SsN};Zwb_ccJDxj_y4yL;>+|6KKbfRoK5h7RAzd4DH zfrn&WSf2QA*Nt07qZR!Hb9vAf!Q@Lnx#G^Rc>TSQ$*PYCiwR#MP3)Wrl|=y*%aK2! zqZAHfh;H$jqtG!xr0$)I_{b(!d7H~K>Pu=2yMYbe_9s#U#)W<^^b}Krg*!awhP!TC zgS0dDUQ_0-0^c9WL*gMLK{|*hdZ``gAEPoFB|I8@xK8+ev$afp*;g?zy`vJ*HUlwP zgL-)y)u~w_1R<5^zJ>#fDtFSpuyF84P%x4nx?lkGr;`-h;7uS>3{EZf1MP%pqOK>G zWvHpn|2dG)seECP&%&hv5xbBHqm)}TIw;kM($-SWsJ8XV%bZo^)&^^YgnqP^E|7gJ zrEH-qJo@7}z!BA2UBE^a8`>~sD;l#2Lp*W!pfMzfcE`^?!W_1#OqEywo(1f8o6Bp5 zMJwL9!m}pBF3;-%Oqi=oja&%hoG9e} zSZ^UdKW{fbU-N%yefEz1)}O0xkQ=M z?9MEQzuw|jDkZ}~{y_TS+& zm0WmSF^D~F43_Nu=>4D1t4Nv1A&L@w4M#l8Dy5u z7SM}-cgLJI(P~<+w`>feSJNYL=+NC}V<#JK0Z*__Vrt(dJvr zo~lWBY^87Lf*D)%zW*A`GqZ7c_IPXsxp5(s|Ax{O_)PH{CgHf0zs1ImXD_=T841% zU{pQXIB>;%TFZRs^-kR{$Ed@ya_)htAj5P73NpNOYMNWQB3~4aZPI z=4!D!uqCj~4$%8VgCa%!&6g)1)Aeg!f8oO6?-GHAmgAvSqefr?*JYkaZ5oXfyWIX` z?smsG9l_4xeTcHlqEg+FRYbyP%m1Nu)D+2&>?5Q9?D?IbJwQ1ip=D6J`3;Bzol6hP z)yRfVs^fMYp18)4Su>@*ZFxTWeZB7d%RM~pxGu2?6ifxONp*jxzrIf1-v1RcxKG2y zLxW&_5`eh9rRTjRFW7Ahp3Lv}WA)E{ZSmIeo8unn7W{iUM3dWqw%G`SKQ5aN{8Ae> z7$3RwG?$~YELX1q@<;GCDWkxQ*ay$vz2sh!FL@}U&mEau>%he_V{eqRM>S#U7Lea2 z*Xupy99JCE7iW`KteCED%BEK#TVCLFZs(u`rAP{3LWVbl5R51y<#W`+w3K|Mal$eA z$Ryz^s4|stmYIV%3#4Kjq$9ZthmFl=oVg49rOXXpZnuzDel)w@ydYj~*H>F@_Zipk z4Z|vW@G|Uh?k`n2I#kTia^Z9C6aMzK|3t;QsF)p^9G-?WLE)khGM?dYNWmW3Q9!k= z*Vjxatc8SkG1q#3b8?t0vD#K660KPxM(b7c-ETyqEXYPeBV3ekx!@59KwbV&^?4Ak zYhhEHcSx)FJW4G^#i+qi(KIU#-$S|nq zT;1lYW%nSQ3xu^b<12dpqK+@-TLDJ1)hKTE{(5fq=YdV=_kBT>kVyJ1jx zb-<_V%RP@4@|Z^7P0ux_A@H&6(BLuM?K|@`3RW>3&IJ!FAIdzTf|PwCU65;=R+BMvBm!wjs%N zoXyTs(cm-oS}q;wLdQ7kV|jc9bRbl}o6{J|7F$LO@>TYpjPG5K1sIKjne8}=F}<-* zJRIfY(2}_gO9ZdoTN*`zztXn%h?7LALdwzl?&cyjyHkDjwT>gvWN%o;t3XARK0m) ze9kEZhW}y;e)$PQ5XVU2y1F{~?H)N`geJg&!#KI$rS_t@lvsz@QHX&4k`)Ely;Q;3 zKf-v$K;MkiYQfVMjl>yZo&Ut?B->ma)qrCH!pHmiS_Hn+v|u^SCidV>Rfo++OR4z} z^H^N{)MNLr37rslT+*hn+MHDq3fm9Fv0EV^m#0Vppxefe|MQH1A=8I}9OZgbMeaGjf<@Ss$|^@R$mlj`QJT)P{y| ztR|wB>&;vyPiiP}**MANuK4LD0*_FP91iUte#IMQcmd7kUV#po6-j2Xg^!7Tz8ROZ z_pUUb1EovUwo(%ISY1d6Kv7SRNA?uI9%Dn-3C)HoF@ELaPqOE3U-k1}b^*BOKA&d+ zc_BgpPCQ(mdFL#<;o?eQWF`fMNLq024yugH)SNBx_1k$gpnSWkwI8jO*AKV+FrM$s z;^pvoVp4vwC`*u9v9weDzyU%?s0bGl27*_1;Ub6flz!#t_6nhkF}LH`hTP=z)YkLT zKf#yfY1h|!#b-ng;J*E8X?~c`7@^Ki6w-B@gL*dNV#)EEd$)o#&uv4TX1{X|NB(|3 zAE@h+*IJj*^U`&Lp~=siJgp>$9oA~nwUZrOCZaP%>yOCyS{a3;LmL|748L}rM}k89 z(hyBvnHf>O4|+`$6p9OXuPnqQm-F!i(-n&AoYUMgz~(yyzXkEF3Zd=F>}ikL{I?}; z3S+b-LrZT$JgnVizA+xs*Ye5bKtBOo%XF0CKPxuo6mOkmD1w2X3gO8B>=+%VdAe}~ z(-YKtP1%I1(0&XB5IhX-toaJXNUn>ai-2C!8dy{v<)l$~KMaQRFck7kBNH9(S73wX z7jR;JQ79@@PCQBx=A#0MVbrI%_jF zmN&GrsC)1$3plxM&x>i~TAvKp|6Y~Fx_VDk6l5DSF$y+b84FU=yH#HpDZ_ohS97+R{ey%qS-8t<43s?Ipe?PLsEIlW6&hGc;MD)hCQcUv>5!S z$9NLDLsObxozK_((Ee`^8rH>kQ0U31Q6s^`L81GaOYEOrckdYfVGks(8~!{u)Oi9U zSjC;dh8#{L>RYM0CO`@PAUnLb!!9kA_~UKJg99&eu1W8`-*YziT&~82 z0RB$7TSJnGdu@7s5^g<7FspuJ+-yc4DrL8UEyN7b-&_)2TAD9snRENoD2aYLu+0~0 zx{`dp>e}i!GwugJ50rC~x9d}jl$_{caGNWm6}jL;rDwEydjg{^tV;vq>ThT4Zqix^ z=iqrar&rKA@@i5Wx<~VX%Hmq(Y8e5a>@pXY*pmz9f@znBfeQjLi^oc}=^f{2&u+t5 z#<#lC`x57jP%dZ<3jgUe|E^@^eb`{_KW!(5jvsjj)nd@taCt#R#c_det;jT#pP8ld zC*~iZMX!5LdAOO!H8$kcdX@3T7pQ%Ihd4)&yB%=A{&l^WFiAWxjd! z7a9+TNmVJ7r8KX8LR*4352G}rcA^9D$=evbFabTxv41$8cA_CMz(Jwq(<5x)&z* zxaXiOv|?|xC9QV~G=6fYfHxb)jShHZ4$Hjt*Q3}r+nLDH^m|>Y`Y}V7#kzpIkQqS% zh|{+>L^n}=1#YDG6R8Ak+!T+k9$Cl&^Za@XSiR4A?{?YuZ5H^eT-k09p1BSQ`v0|* z^E}%GXEm&;{?*z+R{RIEi@ul=Oim$RqOp$i{7#_PWByan`cnOmaP zfe+mZU^LTv#z}Ac;jOo25FL$=@(a(vZ)>G)#(1sfUZJvR3YGO{qUW!1?={4anO7TI zo4Y7nB=Q}jh~hm5jPM{~;<3EBroGM7#d%bZRv;!_2pYuAn9>yle>1=sdse+@^+WA_ z^KXsOKA8fVBi%o=ecMU6RJ+>)+Y=f8E`bLkeulIU7F@`B1@QZ9JE!kN=w2DOKz+%E zO(xNLH53SSUVP5%3%EHvdrEGUe|uY&uUZyA#9ugGJ8HPLf&5Q3U@lv6Dc5=PE*_F5 zi&5uci$S70`39;02bwLtHM|9fS?ef;;=LFi-jxj+JntDRQ zCc<^(Ng@S1UplAIfnhH++^hz>!f$ByCwxj%X4GiGFf*+GnkXQRr zhOmIn|H}{gpSu6&`Tj3EqVkf%h=V=Cg{=Zz?iKtMTkAvy@vwQ-!lD>_8SC8Yu&fMjPH2Q?B@S|tic-0`%zW6wcS*;IE`AOQ07&uV z?h}AVcXF{d!uh_oW9Qc~3H(EL{y#a|{|&DGUufVMU6`4OW;zGe&t4=uUPttQtEf+xwod`V1w3_xn0{rindzA6X`{J+rQ0IvTVFZ{pT`Ndx< zbXIV2F{V^3L#+j}xbW+d%p|E;CL=rNQ|1%s2R$~gEQ-}iXG7?Oh4i)NxS>1#PZd8z zG9O02BXWN3SE(3M0Kqw40GBN(+7E#o^NfG6qyhSdVF-yI450gd^F6-s_yBSGZ? zxGI0d_@R^ke-#f8>|(hyyY)^El`6>+Srcl1xxE#ji!_nw@_vkv%O#WXDI0{Tc^D1G zIpD<%(c~5f-jbL(4D51hQ3qi0SoOwo`MD6_c*1<$ZzS>P>seu6o=u%M`*0ME;Rvi} z9vQ0u3wv99B(X-zAsySblpE(6xl}05Y{_CAhwCqU05|n==}2i(fKAK6Y+ng(9~FhE z#7s1qm9$u~D1yNN(acu}G8HT}XaxeVjFSsRvRX1G47x~_m^3!FtQbKgHg2u3C`3&d zUWU2*8`+#h_6{sM8>H`|tJg~{^WA6jMALxL*5YOUX)Zt4Ur?7*Mg{iIdLhR=uWYyK zPyJ?zL@`lj|1>ys4p6z@5DsJT3h%AQn#gw)M9Y~PBQ`xb2!6?h_oOv=vhSDw7{^3N z{(&LzJg`hHX>?HhTlC$;+*bs}J8EfS+dN-o_J4H}FFe=1)xRX3PE%Cro4avRq=4exh2(cCPkFG-Ui8chw|jJQEEY7F*)1}6!#x|rr_XYZxX zUn6XIF;f{>brbM{!b8eFbyM5gb(!KuW%}^PYkybYKNH15SA)*Y)1buKOXo(l$(N{S>z_&9kiFKG<&WNjW;JF2BpDI0goLQ`-V&_**}T>O_AK<(3HGj9R)LZ6$aEPNnZ9%tF3Lr zNmphpSXMS8sg@!R1&2dbI&eFbCB$_FY~#b)^v4#D$K}gWh?J;F8nSH4cmuxb(?-M; zAMxk+aeq1k(Isdb-Y1B(Pk*6p(jXvtGL7DNb9Wju9<8 zKZ+)pAnmQqG3eP?h)67EHLItRK@d(sT)P4R@l-+bWaytUGf`5X63Ndg$~@ zy1a#{cJmT+qJp(*n&BN95;iTPJ3q#3I#lH6Fn)U$*3~nOp)g7_E_rP$-O%pmOe1&< z*}`%`keYd^<c!8h-$M~GpX?;AX_kF4Y- zXBY#3620b0)?t=UhDF2Z{G;H|)ar{!ZX4vZXdnNQtMJXyJKK9m9@T>6f+ama=XP(o zklgRzW_%}yQ)p*boN`%Wa)?g8-(BNL2YDyu!3W!Pu(@s5^KtGST~H#N_}5MC!QQ~`I+hMe_fb8RjU2v zOE*vdbLv`F?kVp!26E;_A1o!iIoDoYGjyd*X8ToZB6y&u(mDUpokDZHoO`!EB^U)y zzQ$~WPygXkc(yUF-2 zrkeD47wBAl>7{9MTYr)_J9(^lZH|m`{#50@%>hI$xMo#%Y;lYdb@)`v--sq&Of4Id z2Y<*gxL+(es^qEk6b?yP5mmHxt|R7%0oE>A~$ET zecaC6bjniH2nMHG<4b4$Z*+6m2F4+YW*6!WwKDgPVOQ)=ZzX-zMJsK&MjEtlg4%4| zuUvgol_6g;H;C3+_1vv~<9|KH#$qJ~T}H9Xp3+gcUhr0kzakE!g2aG-RzOb3+*z9P zj`H?$%Zas&VRSt^&r@fjV@N17bC==7-%F{zqu7|Nr`Ycy>Y@?>Xe)&QE7+FkJR1(v zP%5TkaTNd}B9ZPdp3~08$or&k)9bgM!q#ez#RT@GAx;*t$gkSa=Jqpy`wlcmghPswwahUu-8 z7Z2O34FN8O>2{~xiO*HqOoyAeON*rEB_F$~e=FKytiZ-L!F1jxq~?%H+p6X82Z=~` z65np(WkQEXa92`aDL!`)&%+H=1HBHQ7K%;YG{;SBFWx_}PF zwJOtW+wyo>AkWx#)I}c3%4Ypv5cEV_OPtmF@s{3Snvgb`;i;%pLwjRC0wKMYCg6EjQn3z&1!217>|^=L={ZnQ6v1|Sa& zYqr_lIx$$dLdyC%El~T6Jqf%~Dfk4)hDqSsFd4Rr^^h+TwI7(Cs(n8dmp4I8wmQ;yWYT5m6|SY5$N5s=)7@Gjb*3-g#-1+v)}vu5P3AF-{AFC7R(t|0(o6;!xz zOnRjfxd_dAYzL42k3+X~!6IM$i z1wk-W;Dc`8pn3(K-GjCQ*vRL0Tlhm$ThtaR!DiUJ)>k=dqNm0x(>U-D;16S6hnX+@ zG7_$YKJ6?FUZAjR09!i1bSqRHhAJ!P>YX(p@$2U7ju+>qkf0kkVJr8u`>va9?I4Sc zBzwzI7&?;=#RllBXv=gGWRU zZY|wuWq398&+C-co1g?@bNWO^lcIy)+s1AITB*oDsm?7pZJ4|V44W3PLKn!5&;7aM zEO|()IOggwN0DLXQj|%Z~)zsvZ!)M28thM`(hs-(7YqR^Qf=R_(~|< zqKAi4xEhk|O;?$TQT5Aj=2q&*0J>32H=2Dko~+*2+Rp<*tCMETHCjozybT&Fy1R?z z)zR_!U|dv?F$TyQEz?rnP6hswUk*sUw(Cg$eQmM~JZUe~fwi(swLaJ68jQ3T-d;`X zXyV8ife>&2(`vMIEpEN3fp2J0W_>7{r>+(+$7^pG8xJz!ge{t@&dz+RmJsmtK>-kh zyXcoj<=iY8RBJa>`mQybd)cpJw1?bHul=Mp(Ir|NO@hzogE0xCmHfK_c=2GTCrLp7 zpSK>&v-Rq}JZXQZ7e04#lw1>vgA>%LcnVEZJqv1;t!@R{(k5C)!O-A?zU3Jl zU$uz`UhG0Jzzx5ZfqheFa5({3T9lSdJXb881-5Z+hHNKvZry1E7PhM;wwAeTm;Z?s z&{=_dE!2rBP1IqQsiS)}PUqcHEJIxD0`oVf=TCQ*?d{oqO0z+%nD=*mJbPYnR`EU} zJqR(ac=Q zBfa3I>#_+1{7$eZk_!gCr$kf zMQCnS6E{si=MGGng!D!8Cs;-a=&-t{>=9}5!j;Z>3HUOuogPnFE2Kpdxi@onWvzi} zv1Hyuy$QOeW2z*xo}4p$2$aT6PBzhYfkg3Cn(65vTRozL3`B$3tNOcnC)ZjjLGU!) z_L~^LCfalNc|9>F-u%~_3rsiDw#$G}G22}mz6=_?r0sujKJ;0Kcwg$EaTYG-MxB-9 zoa-#yQRXSgzp(#OC*VxpD&UoCkfZ#A-pv8x3dse$XMEW}3_aC*dn7HkWD-$=W*TDM ziQaf+6&(89`?ejSd~wN|?V5DdaZ7aB@{g&pYwBc^xXey=%Jz5b=T7Kcw`gWhU8l;A zDM?wl{K?sKLRk{-!UBILL6IWR?88-44IF1(exyqNHN_H3*`%s|$pQ6Tr_$TcnF&z)tm)Se6+XJ)NXq#x)@=`Bud^*GCHvi7$imrl;E*DK9O zbHgI3QT(K7nabrXh-?lX#(;aLEdGjc@tUIGN)@pBu(7La6M@fLV^6qJGPII^qRZ&T zr|hm)egIkVv}7n}tXAG4g3`Usx4(tiO{y~KV**k>PGYydtfP1|f!X8o3pg1nOqh@R z{#VwqMn9JbgFPY6XDMXuSemSye|yd%;K})@4V@s3MTZn$e78alRPCygY8^EV`Lz@J z4f|m!LiWxKf_w6tL*aW2K5nu#a4%H&1p}$-!6%_I3%S+Zc`TI;+OYu%J^*+JADE-o z*HofS3RUB8wqhNKui-R-fNFQ0{kQ+$BqDrmef&BPd(lIUw7=`EHEnyp1IE24mn8n1 z%(OJ8kvqzE)XOtfolMyHQ8WGV&t66v2^D+vMvad4DA${mzRK0OoW^f3sv;d%*&^;N^>D&32QUc6EXrPc}#EI@5M%gZ)fTviJe6blcXx8;10x5l%E zU#fmrOFZYEu@aoWwXbJV{$9v@lgEblqxI*&SMBXgc^fJ~-5KzofI~IscTCvg58X0ZuD$@Vz}gbn>}++OFAl zJkkBZhr3QW`MiorZ)w$^61O8DJ8zO0%4HWOR3HHCf}BB&3m20LrS0M1mSL6m*7yD5 z|HTootb#v)t;mZ6bDiQA^DTl)yQMg}BewbfKzNki+tMXvlP*qf`QH*5%4E1E6m8!@lf@8MyBq*t=M zqPZ2ud%O6#NrudOG(&&nM!r^lMmSGacRWejLBq|j_ck7OM*Vm3gxqaeO0^KFXYC~C zUF>_P7>e&{iIo*s;p|y#YUT;wk z-WavAB^@`o%aUa26|rnDh93w?4T-LH#Y&x1KHYsG+$EvYD=o|WLur>?T3awDJi=m5 zhYSA$2M1|&zP$GDI1KV`CL4nq`GrjcaZiR2j%Za)L+byk3+58+p0PNF?;dpOk$5^n z^H%T-qQa|9C1B(Hbg%Nxex&*Y_ToMElVrG!(HOIj{M$eQ11Rs}D!qsFsrplCK!3Bl z=;&4lPA9-T=tj+~9J}jcQcmouTt?pHy0B_WWOJn+hpjDS5B{QF7X?Uv`SwLyWdcH1<14gs|$de>i#faBTZ`U)HC{Y6eyAACxugYj;KhNJfPu0c?k)nz$v1T4&b&wKI0 zIB4oJ#gM>uOR^0>_ogzK7j=G{EtT}V54V-J`{3qFA3|dttoUJ=1Aeq$xcu{Z+U#8i zjRWv|et6Z@WRT*nidP5>&(mWtVoPF2y_M+vaihrPuHRL+wi^ze;eC6CFdrxwCh8)e z8!peWm9B12;5NdKR#U~xE1rs8U};J#ihn$3|MO%9{&)s{1o_b()&CU#6#PFw_+3jk zM!p)o!8}E;6E1E>7b6fZV%6)Tz~2dtx|f(?T+?Ct2Q`;_JN1O*0i;l4xHt|>bi1#` zsFUgT;6F|^HKL{aJh)VmiY^4W!paYBup(uHxXO}c!fy$eCUCqoI^U>KtF07yV2{5D znL^AGG4c6rWxHv{Yc^eA@}?oqRHZbf^f@4Ugj`7H!R~)*cv5|`6=9C4b?PIcwN02r z3rp2ojOa!K;U$%ql7!9G$uTmEl3GhA=ojj8aFv%Ih0dG`ff>jDSbT`1!NM8e$p0tQ zzd$ZA!=XE#sFn(|gRs_{!rZ1^LRSGP!F{UIbEg`O9rV*71*7^}rOxB^;r8ro=uCEH z8$~zki`UHE#(C6U&DWQryviojhBRUG3FC!)#UC$H8ZO{t>zycjjLe`EJ^Kzx-;B`v zs@~J2sv69AH(-M!jSA8mPJzPLBaok0Bl6%9%~-_KvKdCM(+Op7BUzTC3@-flsgwO= zcqT-;G;y>fteq{yu*SdMpXPTdQ@knY3$q;|ae$n$A-=jr;&e1w?-d`k{7#)@3#sE< zzb`Q~_GaMZs_jLc^z$78a;4DNyTeX+A0e+3gF^de2dRW6O)8VP55g^NAk2>zY_Cvm z#{ts$hvp9cNEX%}%Em}ys6F~>IDUu;YeqZ^8LA_gv)UTR?q&=r;ux|9LJ>JvM+CDz zCx~;dD7HG8ju()=c8%j4sEAk!E6WORP}0Rnz|`kcSvhGe-xZdxpZbF8K5Sw z=UEm@CvV=2}QRK8;pXxDb2*d*;_t6;P3EIod=;NuTGx z=;RcTj|@XGe>8GK3&fw?25+m-TrMomCiwE7y}r#0=(JA`-EDznZI`fETvJCpiN3p| z596U(qkcyXy&ip~@!^al+{L~jx+3pg5|biDac0g} z&UUX$^n@OrK39op2SENhb_eZC7G<$$WLJ6-v0TVeA{M2`k0e_j#pB?`YH?#J|H)Nz zkr=7S4CCAc_rMulluCZU& z;1xO6y|Ns%@D9$ANnUmFg6&PAs(RpnLh@WA$UfQ)@D3*p8w74|p^jGg;~9LQ1V9pi z0C+*7q~qi-+}pqy4TPZaR?~2}I)0E0t*Q&N5au_&6mCCp-dMed&9x3X^3HcG4C#GW z)u|(TbWHhMMR0Ijzvj7cibkw+SyaThU&ZMhd1}g;u~@okPDmZhEy4)>0(~)h!~!-g zXgUyy8&hJNqRP9GaH2uX8=-qP3ZP6F-u@0bthw-%AVbWwNSEZ(!4&52u4W^a_D5YA|f{i-DM}xmk2TP+jeyY4<%bTS=Jpb0H{9aj;wW#K`o+td^rC# z`gw<2vTCFX1^@$yD(>axY^qk^63zu8boxNmc8vz6L1FC!x0Jc*;ET*;FEvnv$t?Et zA7jL`SnpGNMowab!ekHH;ILw#($|Yj?92li9^&ExOTRJ5u(t!c{0a37`VjEt)|WG- zGy^s%H4Jqe6)7Mek#7xC=`!6%QmFD&H?2jgaid&o|j+qxY3}0x_=FkjvS^-Kzkf z9Awl{$L=YJe{N=e%WWa|zj{m5xXm=}E(t8ZbD%@`_&3=gR{zw;WC7=a)|zMy1e8F; zRAMAK^c&lC^D}f{_cwyXT?&EDXZD`>b4rmug=F%Hui-n0mqg4jn|xRj6qP`A+c2H4 z1#EvexLx)r;7VYX4?jp>8ra#<=$D0wH~cn|g7{^0pBNLgx4(+@>i|fg&mTC5z@PDW zSKJ*-nvk$J^bb@#ZcPwWvfo{K`wE*!cCc(%{A~5BL9<(NvDs-&d3QPBidzEMOGHpZ zvy_)%^*){#evQ~$rE)@0)RX8lqK8%-w&%iH-K!hy5oK{lPkp+ou4iYL2Ic4EU4mO`C?Qb;E-T)fW22Qc6|;OP^DYb~@u^=ZD`C^~9SN0uHR*>T~?w*0#l z{9>GuwL!hEvm(fJxZJ9BV2bua#hW;8!EV%hnoyb&J7L34-wgCM6Fjv&SQpF;=Mg<& z-lW%Laz%3=!qx)Bi5j!4fsyQXhKq+Uf}an zU@N7NPo_3S>=r*};@_ngTRo>*p(m(*Ft;oIUU8IQk1#-m^U8`=qubf5pPWXqM1Ve3 z-`kl?lryfT2BAN%sttGguqs`5!#%D>VnrXr_#;MTc?fWyDmxl80t|VSji;Z-wT}B> zwV~6PyVCVgn`^}A!ABSIVE2=@a^4k_w(32 z3aTB9ZR4O(38phlU}D$weH)G?Gb&J4=yLjRZiGQXl)W2pJrf765gy_jMlZz4o^Pak zoWo1}MWTEa#6LD~Ju}15EybofrI|_x_s4t7 zSS=xukCIl?Wno$1Q!E91!#O&^HMrOcT1fk#pKFTsRbY?L=1V3oI-)=EqH1aMg6or@ z98t3>*+J_;tE2{;{j9NT{0$5{E?$IWXFLI5$y(MI3h8r%%dcMjj$ceeor@1!Nokmpfgn>BQVS^x*q6K+WN7?z*#9JVFR1NW;loG4j97a@PFx!|~iH z+=Qa(K;&4CAI0?d@)c#)E2#=i8!A#^mpU(>LuHB0Nd|ww;DiMp9|)@z3W<6qaIMqn zLm4LdO&RBJ+{GO7B@eaj7Y3J$&CgKo z;pZ(Bvm8;^T6Mu$^MRoP4IMr$O!3Iuv;sY$0oN1d?|wok{)X&n{x^Bl%nz~0x)VEiw&+N|8Y8MkOC#~n?WTQv|J$ys_3wKY&1 z@L3Y5{skcT35@(G=LaA~9l|Ny;e4&w8zBXnkT+pDf@=ZKdpAv)Pj~;$#myrBdd|ng zTm!3Mloa6!v~iS6038J9F^581T+JUU#;nH=j+1#J9v5oD{_Kb0OZdE^DlkhLQxPM~ zs{1t{=yU4q`O6{4%MEalw{@^XPr$~6?-V1J^kjBq+cSFVj6UOS^*DoY;}GC?3om$vTuryAt8JGPFR>vXO7QabkEo3mDMq;mbQk zUW=_ES#}ic1r)bjoyI=BT@|@-YYvR|x?EqB=4w>n2m8V{T<{giP0fAnQ{z&c#(Typ zCA8=9LEB1L5^@>s&$Yj0J{7q<>NP*pxF}|Aw`aC*Kecbay&V+*mS)`}^xnSqt=F}c zoxVjo&u{d{%Kf4VGQ{hQJvZtjt?6F!*zCUe)m>iSF;_{%TAr-8b{f*H6V>{RvoTT> zQ1jtjAs-E|+`qX0;JJvLdbCAq+G%i6JsVV9;Ta|SXXICAU7)fLR2C-0LiuMlU8P!y zLc5{2acP#S`WY;RjWvPoIgHtqmHx}$08M+r7jnlcqqpgta4XN`g-qqGzNNr4EfsQ- z7QPlj4QZ2y2VxbcPd5`6=f@(}s>HI+YyfO~o0UX&QNM$ZFHBwX`2H&R$oKeHC=mD| z>J*VBT9x}m=_`Dg$X#Bdbaf9N;lO55EK}lw-NrhzqRfu(n!=+<=?!O2~NIz zK9}@WM81RsblR3QP$FqQ%75=aF};34oVETRt=;KA6p9`Oa85^&;&c?UG&Qo0C3}f8 z(IWdgGck-)m{F27%1%itgc-|dh!G}coS4BFV-1;M(lGWVTlOtu3B&Eh{e14dpL<^1 z7x(uMJa2z*p6AO1!h9=r7@2S4cH%;w7#L$1o*B@`6)0Uyyh?J-=iOSfy%M<^JMAW) zcnr~*HoNztzo$KW;7;m7q{84ldDWTM9)ltjHKfy?oYJJ$8jLHK$tJ~Q+1>_?3(wg_ zs>Gh=<2y<)yJqYt`M_NRyS6Fil~dvuhIniGg3}fqClB-)6TjF)$a1aXZvJ8vU5{zI&Y&q|4}=2w z9u;h58Gm)d^;m>cy1izUleOnMbCAKPL|c9}y~}wb;OXfD;fTxwR8{+|9#u)=l4?>O zOmnX5K#w*w>30=pk5$gE2tGXjY`mZa(&Ss>EiHOf{q-U+B>UIPhJm+aibOxA050F< zIyM{;E7qu<_JiJ&%Fq^n7mdpT!#B>#2&@x-Jt?I+!T-!p`0Wz<)7N{*5!fxLNi{5# zlC4~U#I)|9^1{bX!XKSK0@L02OW~_nG?clJyCu+SAz$IeuCGXhRZUa@SCe3Ti|yO; z%8~j5MlyP4hG^93nF(hdPWgkDP+4#QcKX^cLyiqPhg=Fi?46xA$BdmQLO~R?qsj5R z24=PrQza@YxtNFhMfCLppTUmFN8b$}?De1Vzwe!Azj)LC&V9n^#kafsBbYA)(*ezX z0|V#dj6pF-(TC9_4Y6U_BfEz~islYuGc#?w{$zLIk^no0>Y;nyTaOnDtcJ-$H|=a6 z)jLxXmim*H`qsm9G$LKZrFtYUE`Hta3FtTFOoF!4UeFY&KhUxLzXw`ET>6NNkx2mf zyqU`Yd|7bP>_HmT2%?6v;3%v734L1(_Q$#H^0vp#R4kcUZWmcm)-nosi=DZG#S51F zi@cDcsH$B5t(sT3v4^FDysB15%^NNK08*r%Z|cGk506}^t3xt`%f`*6)H{op@u2Fr z{WBtHfosO?6ICAcO_BID-$LY>#Sk@IunnB7>iuA66=m6XTK)nyID770oTw$7&UnKf zLnX-){CHtMKCReft+x%KYz)y4F=b?*AY&A(FGT#?GOH5@&h<^iC>{(`T=uhuiRW0{ zH2)vGRMtQY$U@`5qR$PJxapu?u}`ZGPfU$n@e0{8*c;Q)rO~F;x92@^AClA6)t5t7 zjxU9);jbj}%dN)~@>0Tk7f~bU00S_;Q(n0EEleB;mXl6YZG{aZVotK zg27A3fjTK2Br{#A!%z;dTNYx}+Mt6<&VgIDvw?F*13!=BX#}MR#bpx(N4)*HHZa@K zoiMQ+;L*kf_z=DM$_)KJdD>6$m)M#EMS9F(nXC<|kh%nKA+&OUbJ;6r3H8~U+L7gB zAyZary^mQQRYB9hnKx84M@3jC-j1)?<^~!z)Oa306;tu0M2$0TMa7y3;Eu|Q;ovoR z8y!e8Q;8Ycf=kCQBV7=oHk+EiZ%UKRzxibV3{Q&Q39JKxZ??D1Jx|(1EW4!Vy5^H_ zCyV0>Jp4KSH>8WAw6!UL{({F&>Py6rO0~~)AmQ-(fQ?Dg!>9G*A zlM{DeO_hrNz`Sv(m0?yzFF++WQc35R(mQH0$<4}bEL-~~iz>$Z=OqI+GUsz~4jI#( zRr7-GFV{k&*jaE#$`(u06kxPv&dN3l7JqO|()Y8fE5l>GbDKwKibeHL01(*Yp>8%h zdeW3cmlV;6l))r@a9+? zqMF*(Lv9Hj4NN3s`W{8np{#d-s>SO}+3Tj9+S*tPrH0P7aZI&?_#tgCwVaeXDmsiuFLwP0^;_wOeAuZaQ)u61KD{ zPV3+4bHYl<0<8N`-f*IibaM1;C4K?nd&^~0Xq6Xxddh6`O@V5=$9qUG zMD(Ylt-A-w8DWKYv*q1PDvl4G?Yo*fvR*E9a%;LAp`IqBMeVRiOZkZ(jq0F8m4cm2}Dlx>bLTZ#CTUm3o}^3m-WD<#Q5HjEm_KwK~8FzZ=|` zGqGLG=Z1eD_Qmek5=LvKD5Eer`)TNhH7e0(vZd^}(Fa5mk6P!hmWJluprtppXfQV@ zi1pxedcx*<-gY;0U`5K6xY;veUy^;gg)4p&H|bWl)%v;{G~ETv^WEV#tt#{LMYJY& zd~!7DejeUTc2GRuJDi>mj-ak)y{~iC|M_|b+*zOk>6yJ&El^Ch*Qo!X49NhZ-$jk% z%H>xH{w!kulCQ=&LFA3pI)h0#^l4h55<7V4))SK0)p;A^mv^_hO@-hSo+r#3^^OYQ zeeAk47H@7~o?EB+m@=Mau><8YFl z$qQc#Og1DYYOYjY_G=7FmJo0}@->d{y=Xwgk;7gM%Ks9d{{wgaZ_@hj4C{Y`t^ZG` z^gnYO^h!kdBxNdXjfD9mB`a-R`S=bWMsxVDP-OmI)Bc}$>u8DegF~RrH$B|ij}v@9 O&+NMOHIm7nvHt+-7X&l_ 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