Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-11 21:09:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-11 21:09:22 +0300
commit1bd9d2d9499d0d28e62254a28fcd3d913a8704af (patch)
treeea9969a5a4c3ac77858be20d69869674bed5ca43
parentd8877c12347443fa02e0ba53ad8d5cd318f6fa28 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/as-if-foss.gitlab-ci.yml63
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml17
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml3
-rw-r--r--.rubocop_todo/rspec/before_all_role_assignment.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml1
-rw-r--r--.rubocop_todo/rspec/named_subject.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum4
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue21
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue30
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue15
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue34
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js18
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue54
-rw-r--r--app/assets/javascripts/diffs/index.js1
-rw-r--r--app/assets/javascripts/diffs/store/actions.js77
-rw-r--r--app/assets/javascripts/diffs/store/getters.js15
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js21
-rw-r--r--app/assets/javascripts/diffs/store/utils.js28
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue4
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js3
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue9
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue9
-rw-r--r--app/assets/stylesheets/framework/badges.scss53
-rw-r--r--app/assets/stylesheets/framework/diffs.scss9
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb10
-rw-r--r--app/helpers/merge_requests_helper.rb3
-rw-r--r--app/services/system_notes/time_tracking_service.rb23
-rw-r--r--app/views/devise/shared/_footer.html.haml3
-rw-r--r--app/views/layouts/application.html.haml3
-rw-r--r--app/views/projects/merge_requests/_page.html.haml1
-rw-r--r--config/feature_flags/development/group_analytics_dashboards.yml2
-rw-r--r--config/feature_flags/development/pinned_file.yml8
-rw-r--r--config/feature_flags/development/simplified_badges.yml8
-rw-r--r--db/migrate/20240111134328_routing_table_prepare_async_constraint_for_pipeline_variables.rb33
-rw-r--r--db/schema_migrations/202401111343281
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/graphql/reference/index.md6
-rw-r--r--doc/ci/jobs/job_artifacts_troubleshooting.md23
-rw-r--r--doc/ci/yaml/index.md10
-rw-r--r--doc/development/documentation/styleguide/index.md2
-rw-r--r--doc/development/internal_analytics/internal_event_instrumentation/quick_start.md47
-rw-r--r--doc/development/pipelines/internals.md2
-rw-r--r--doc/integration/diffblue-cover.md11
-rw-r--r--doc/user/analytics/analytics_dashboards.md7
-rw-r--r--doc/user/clusters/agent/index.md6
-rw-r--r--doc/user/clusters/agent/install/index.md9
-rw-r--r--gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/listener.rb4
-rw-r--r--gems/gitlab-rspec_flaky/spec/gitlab/rspec_flaky/listener_spec.rb37
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/terraform/modules/v1/namespace_packages.rb (renamed from lib/api/terraform/modules/v1/packages.rb)65
-rw-r--r--lib/gitlab/database/partitioning/list/convert_table.rb13
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/page/main/login.rb3
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js3
-rw-r--r--spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap160
-rw-r--r--spec/frontend/diffs/components/app_spec.js39
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js27
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js146
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js39
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js103
-rw-r--r--spec/frontend/diffs/store/actions_spec.js124
-rw-r--r--spec/frontend/diffs/store/getters_spec.js32
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js57
-rw-r--r--spec/frontend/diffs/store/utils_spec.js11
-rw-r--r--spec/frontend/groups/components/group_item_spec.js47
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js45
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js13
-rw-r--r--spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb468
-rw-r--r--spec/requests/api/terraform/modules/v1/namespace_packages_spec.rb (renamed from spec/requests/api/terraform/modules/v1/packages_spec.rb)48
-rw-r--r--spec/services/ci/create_pipeline_service/partitioning_spec.rb2
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb4
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb34
-rw-r--r--spec/support/helpers/models/ci/partitioning_testing/partition_identifiers.rb4
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb2
-rw-r--r--spec/views/devise/shared/_footer.html.haml_spec.rb46
94 files changed, 1846 insertions, 545 deletions
diff --git a/.gitlab/ci/as-if-foss.gitlab-ci.yml b/.gitlab/ci/as-if-foss.gitlab-ci.yml
index 128a6195c4c..fc8db3ba974 100644
--- a/.gitlab/ci/as-if-foss.gitlab-ci.yml
+++ b/.gitlab/ci/as-if-foss.gitlab-ci.yml
@@ -1,11 +1,17 @@
+.as-if-foss-variables:
+ variables:
+ AS_IF_FOSS_BRANCH: "as-if-foss/${CI_COMMIT_REF_NAME}"
+ FOSS_REPOSITORY: "https://dummy:${AS_IF_FOSS_TOKEN}@gitlab.com/gitlab-org/gitlab-foss.git"
+
prepare-as-if-foss-branch:
extends:
- .as-if-foss:rules:start-as-if-foss
+ - .as-if-foss-variables
stage: prepare
needs: []
- variables:
- AS_IF_FOSS_BRANCH: "as-if-foss/${CI_COMMIT_REF_NAME}"
- FOSS_REPOSITORY: "https://dummy:${AS_IF_FOSS_TOKEN}@gitlab.com/gitlab-org/gitlab-foss.git"
+ environment:
+ name: "as-if-foss/${CI_MERGE_REQUEST_IID}"
+ on_stop: delete-as-if-foss-branch
before_script:
- git clone --single-branch --branch master "${FOSS_REPOSITORY}" gitlab-foss
- git -C gitlab-foss checkout -b "${AS_IF_FOSS_BRANCH}" master
@@ -29,15 +35,16 @@ prepare-as-if-foss-branch:
prepare-as-if-foss-env:
extends:
- .as-if-foss:rules:start-as-if-foss
+ - .fast-no-clone-job
stage: prepare
needs: []
variables:
BUILD_ENV: build.env
- before_script:
- - source scripts/utils.sh
- - install_gitlab_gem
+ FILES_TO_DOWNLOAD: >
+ scripts/setup/generate-as-if-foss-env.rb
script:
- - scripts/setup/generate-as-if-foss-env.rb | tee $BUILD_ENV
+ - install_gitlab_gem
+ - ruby scripts/setup/generate-as-if-foss-env.rb | tee "${BUILD_ENV}"
artifacts:
expire_in: 3 days
reports:
@@ -82,3 +89,45 @@ start-as-if-foss:
project: gitlab-org/gitlab-foss
branch: as-if-foss/${CI_COMMIT_REF_NAME}
strategy: depend
+
+delete-as-if-foss-branch:
+ extends:
+ - .as-if-foss:rules:start-as-if-foss:allow-failure:manual
+ - .as-if-foss-variables
+ image:
+ name: alpine/git
+ entrypoint: [""]
+ stage: prepare
+ needs:
+ - prepare-as-if-foss-branch
+ environment:
+ name: "as-if-foss/${CI_MERGE_REQUEST_IID}"
+ action: stop
+ variables:
+ GIT_STRATEGY: "none"
+ script:
+ - |
+ git init
+ git push -d "${FOSS_REPOSITORY}" "${AS_IF_FOSS_BRANCH}"
+
+# We can only delete the environment after it's stopped, therefore
+# we need to use another job to delete the environment, not in the
+# job where it's stopping the environment. See:
+# https://docs.gitlab.com/ee/ci/environments/#delete-an-environment
+delete-as-if-foss-environment:
+ extends:
+ - .as-if-foss:rules:start-as-if-foss:allow-failure
+ image: alpine:latest
+ stage: prepare
+ needs:
+ - delete-as-if-foss-branch
+ variables:
+ GIT_STRATEGY: "none"
+ ESCAPED_ENVIRONMENT_NAME: "as-if-foss%2f${CI_MERGE_REQUEST_IID}"
+ ENVIRONMENT_API_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/environments"
+ before_script:
+ - apk add jq curl
+ script:
+ - |
+ ENV_ID=$(curl --silent --fail --request GET --header "Private-Token: ${PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE}" "${ENVIRONMENT_API_URL}?name=${ESCAPED_ENVIRONMENT_NAME}" | jq '.[0].id')
+ curl --silent --request DELETE --header "Private-Token: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE" "${ENVIRONMENT_API_URL}/${ENV_ID}"
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 7db2c15db9b..ab603ecb9f2 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -3002,6 +3002,23 @@
- !reference [".strict-ee-only-rules", rules]
- <<: *if-merge-request-labels-as-if-foss-cross-project
+.as-if-foss:rules:start-as-if-foss:allow-failure:manual:
+ rules:
+ - if: '$AS_IF_FOSS_TOKEN == null'
+ when: never
+ - !reference [".strict-ee-only-rules", rules]
+ - <<: *if-merge-request-labels-as-if-foss-cross-project
+ when: manual
+ allow_failure: true
+
+.as-if-foss:rules:start-as-if-foss:allow-failure:
+ rules:
+ - if: '$AS_IF_FOSS_TOKEN == null'
+ when: never
+ - !reference [".strict-ee-only-rules", rules]
+ - <<: *if-merge-request-labels-as-if-foss-cross-project
+ allow_failure: true
+
##################
# as-if-jh rules #
##################
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index 2429c2f8d7d..21287d1bd2b 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -452,7 +452,6 @@ Gitlab/StrongMemoizeAttr:
- 'lib/api/container_repositories.rb'
- 'lib/api/entities/basic_project_details.rb'
- 'lib/api/helpers/authentication.rb'
- - 'lib/api/terraform/modules/v1/packages.rb'
- 'lib/api/unleash.rb'
- 'lib/atlassian/jira_connect/jwt/asymmetric.rb'
- 'lib/atlassian/jira_connect/jwt/symmetric.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 8ca02a6a1d0..060b55f00d0 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2429,7 +2429,6 @@ Layout/LineLength:
- 'lib/api/suggestions.rb'
- 'lib/api/tags.rb'
- 'lib/api/templates.rb'
- - 'lib/api/terraform/modules/v1/packages.rb'
- 'lib/api/terraform/state.rb'
- 'lib/api/todos.rb'
- 'lib/api/users.rb'
@@ -4311,7 +4310,7 @@ Layout/LineLength:
- 'spec/requests/api/search_spec.rb'
- 'spec/requests/api/settings_spec.rb'
- 'spec/requests/api/snippets_spec.rb'
- - 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
+ - 'spec/requests/api/terraform/modules/v1/namespace_packages_spec.rb'
- 'spec/requests/api/terraform/modules/v1/project_packages_spec.rb'
- 'spec/requests/api/todos_spec.rb'
- 'spec/requests/api/topics_spec.rb'
diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml
index d1078cf4735..1d015869b37 100644
--- a/.rubocop_todo/rspec/before_all_role_assignment.yml
+++ b/.rubocop_todo/rspec/before_all_role_assignment.yml
@@ -1326,7 +1326,6 @@ RSpec/BeforeAllRoleAssignment:
- 'spec/requests/api/rpm_project_packages_spec.rb'
- 'spec/requests/api/rubygem_packages_spec.rb'
- 'spec/requests/api/search_spec.rb'
- - 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
- 'spec/requests/api/wikis_spec.rb'
- 'spec/requests/concerns/planning_hierarchy_spec.rb'
- 'spec/requests/groups/deploy_tokens_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index fa8f44aeb9f..d8303d8beb2 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -2385,7 +2385,6 @@ RSpec/ContextWording:
- 'spec/requests/api/tags_spec.rb'
- 'spec/requests/api/task_completion_status_spec.rb'
- 'spec/requests/api/templates_spec.rb'
- - 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
- 'spec/requests/api/terraform/state_spec.rb'
- 'spec/requests/api/terraform/state_version_spec.rb'
- 'spec/requests/api/todos_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index ab6ddc4c89a..a72575d26ba 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -2971,7 +2971,6 @@ RSpec/NamedSubject:
- 'spec/requests/api/releases_spec.rb'
- 'spec/requests/api/rubygem_packages_spec.rb'
- 'spec/requests/api/snippets_spec.rb'
- - 'spec/requests/api/terraform/modules/v1/packages_spec.rb'
- 'spec/requests/groups/settings/access_tokens_controller_spec.rb'
- 'spec/requests/health_controller_spec.rb'
- 'spec/requests/ide_controller_spec.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index c99e96f3e55..cb5e5134dcf 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -683,7 +683,6 @@ Style/IfUnlessModifier:
- 'lib/api/settings.rb'
- 'lib/api/snippets.rb'
- 'lib/api/tags.rb'
- - 'lib/api/terraform/modules/v1/packages.rb'
- 'lib/api/users.rb'
- 'lib/backup/gitaly_backup.rb'
- 'lib/backup/manager.rb'
diff --git a/Gemfile b/Gemfile
index a7863be40b3..afa9e7e6d5d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -512,7 +512,7 @@ group :test do
gem 'test-prof', '~> 1.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rspec_junit_formatter' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'guard-rspec' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'axe-core-rspec' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'axe-core-rspec', '~> 4.8.0', feature_category: :tooling
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 45c0a47eb63..c95e366374d 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -41,8 +41,8 @@
{"name":"aws-sdk-kms","version":"1.76.0","platform":"ruby","checksum":"e7f75013cba9ba357144f66bbc600631c192e2cda9dd572794be239654e2cf49"},
{"name":"aws-sdk-s3","version":"1.142.0","platform":"ruby","checksum":"79cd888eca66fd2ef3ae8b74d76173a2eccbeff6a1bba62a60b7c7dadc8dd7e9"},
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},
-{"name":"axe-core-api","version":"4.6.0","platform":"ruby","checksum":"1b0ddec3353f108dc10363baf2282f43a5ff7f13d4e25f99071294e78f8a6c62"},
-{"name":"axe-core-rspec","version":"4.6.0","platform":"ruby","checksum":"11c25bc9dd388c137ba4e5e63d64d20092bf22c884d8ffc829a22acfbacd747f"},
+{"name":"axe-core-api","version":"4.8.0","platform":"ruby","checksum":"88cf44fdbd5d501ae429f9ca6b37c4a46ba27ac673d478ab688eea3e353da62f"},
+{"name":"axe-core-rspec","version":"4.8.0","platform":"ruby","checksum":"b2c51fad40f3fc487d3de522e337d96d5144d88b3e2e00b87d2b53d17056d11e"},
{"name":"axiom-types","version":"0.1.1","platform":"ruby","checksum":"c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383"},
{"name":"azure-storage-blob","version":"2.0.3","platform":"ruby","checksum":"61b76118843c91776bd24bee22c74adafeb7c4bb3a858a325047dae3b59d0363"},
{"name":"azure-storage-common","version":"2.0.4","platform":"ruby","checksum":"608f4daab0e06b583b73dcffd3246ea39e78056de31630286b0cf97af7d6956b"},
diff --git a/Gemfile.lock b/Gemfile.lock
index f124754654b..97b5fe4f236 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -315,10 +315,10 @@ GEM
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
- axe-core-api (4.6.0)
+ axe-core-api (4.8.0)
dumb_delegator
virtus
- axe-core-rspec (4.6.0)
+ axe-core-rspec (4.8.0)
axe-core-api
dumb_delegator
virtus
@@ -1819,7 +1819,7 @@ DEPENDENCIES
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.190.2)
aws-sdk-s3 (~> 1.142.0)
- axe-core-rspec
+ axe-core-rspec (~> 4.8.0)
babosa (~> 2.0)
base32 (~> 0.3.0)
batch-loader (~> 2.0.1)
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index ccafec0fb7a..ad4b7b790d0 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -518,9 +518,15 @@ export default {
>
{{ $options.i18n.variableReferenceDescription }}
</gl-alert>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
- >{{ $options.i18n.cancel }}
+ <div class="gl-display-flex">
+ <gl-button
+ category="primary"
+ class="gl-mr-3"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-button"
+ @click="submit"
+ >{{ modalActionText }}
</gl-button>
<gl-button
v-if="isEditing"
@@ -531,13 +537,8 @@ export default {
data-testid="ci-variable-delete-button"
>{{ $options.i18n.deleteVariable }}</gl-button
>
- <gl-button
- category="primary"
- variant="confirm"
- :disabled="!canSubmit"
- data-testid="ci-variable-confirm-button"
- @click="submit"
- >{{ modalActionText }}
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ >{{ $options.i18n.cancel }}
</gl-button>
</div>
</gl-drawer>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
index e287e4e17d1..63957d9b7fc 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
@@ -33,16 +33,13 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2">
<runner-status-badge
:contacted-at="contactedAt"
:status="status"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- />
- <runner-paused-badge
- v-if="paused"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ class="gl-max-w-full gl-text-truncate"
/>
+ <runner-paused-badge v-if="paused" class="gl-max-w-full gl-text-truncate" />
<slot :runner="runner" name="runner-job-status-badge"></slot>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
index bed592e3f30..0dc23882cdc 100644
--- a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
@@ -26,12 +26,12 @@ export default {
switch (this.jobStatus) {
case JOB_STATUS_RUNNING:
return {
- classes: 'gl-text-blue-600! gl-border gl-border-blue-600!',
+ classes: 'gl-text-blue-600! gl-inset-border-1-gray-400 gl-border-blue-600!',
label: I18N_JOB_STATUS_RUNNING,
};
case JOB_STATUS_IDLE:
return {
- classes: 'gl-text-gray-700! gl-border gl-border-gray-500!',
+ classes: 'gl-text-gray-700! gl-inset-border-1-gray-400 gl-border-gray-500!',
label: I18N_JOB_STATUS_IDLE,
};
default:
@@ -45,7 +45,7 @@ export default {
<gl-badge
v-if="badge"
v-bind="$attrs"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate gl-bg-transparent!"
+ class="gl-max-w-full gl-text-truncate gl-bg-transparent!"
variant="muted"
:class="badge.classes"
>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 00fd9f43a4f..698fd3909ed 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -144,6 +144,11 @@ export default {
required: false,
default: '',
},
+ pinnedFileUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -153,6 +158,7 @@ export default {
autoScrolled: false,
activeProject: undefined,
hasScannerError: false,
+ pinnedFileStatus: '',
};
},
apollo: {
@@ -215,7 +221,6 @@ export default {
...mapState('findingsDrawer', ['activeDrawer']),
...mapState('diffs', [
'isLoading',
- 'diffFiles',
'diffViewType',
'commit',
'renderOverflowWarning',
@@ -245,6 +250,7 @@ export default {
'isBatchLoading',
'isBatchLoadingError',
'flatBlobsList',
+ 'diffFiles',
]),
...mapGetters(['isNotesFetched', 'getNoteableData']),
...mapGetters('findingsDrawer', ['activeDrawer']),
@@ -355,7 +361,7 @@ export default {
const id = window?.location?.hash;
if (id && id.indexOf('#note') !== 0) {
- this.setHighlightedRow(id.split('diff-content').pop().slice(1));
+ this.setHighlightedRow({ lineCode: id.split('diff-content').pop().slice(1) });
}
const events = [];
@@ -438,6 +444,7 @@ export default {
'setFileByFile',
'disableVirtualScroller',
'setGenerateTestFilePath',
+ 'fetchPinnedFile',
]),
...mapActions('findingsDrawer', ['setDrawer']),
closeDrawer() {
@@ -509,6 +516,20 @@ export default {
return !this.diffFiles.length;
},
fetchData({ toggleTree = true, fetchMeta = true } = {}) {
+ if (this.pinnedFileUrl && this.pinnedFileStatus !== 'loaded') {
+ this.pinnedFileStatus = 'loading';
+ this.fetchPinnedFile(this.pinnedFileUrl)
+ .then(() => {
+ this.pinnedFileStatus = 'loaded';
+ if (toggleTree) this.setTreeDisplay();
+ })
+ .catch(() => {
+ this.pinnedFileStatus = 'error';
+ createAlert({
+ message: __("Couldn't fetch the pinned file."),
+ });
+ });
+ }
if (fetchMeta) {
this.fetchDiffFilesMeta()
.then((data) => {
@@ -539,7 +560,7 @@ export default {
}
if (!this.viewDiffsFileByFile) {
- this.fetchDiffFilesBatch()
+ this.fetchDiffFilesBatch(Boolean(this.pinnedFileUrl))
.then(() => {
if (toggleTree) this.setTreeDisplay();
// Guarantee the discussions are assigned after the batch finishes.
@@ -724,6 +745,9 @@ export default {
<gl-loading-icon size="lg" />
</div>
<template v-else-if="renderDiffFiles">
+ <div v-if="pinnedFileStatus === 'loading'" class="loading">
+ <gl-loading-icon size="lg" />
+ </div>
<dynamic-scroller
v-if="isVirtualScrollingEnabled"
:items="diffs"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 82b721da493..39f642b0831 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -17,6 +17,7 @@ import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import NoteForm from '~/notes/components/note_form.vue';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { fileContentsId } from '~/diffs/components/diff_row_utils';
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@@ -110,7 +111,10 @@ export default {
'canMerge',
]),
...mapGetters(['isNotesFetched', 'getNoteableData', 'noteableType']),
- ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled']),
+ ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled', 'pinnedFile']),
+ isPinnedFile() {
+ return this.file === this.pinnedFile;
+ },
viewBlobHref() {
return escape(this.file.view_path);
},
@@ -206,6 +210,9 @@ export default {
diffFileHash() {
return this.file.file_hash;
},
+ fileId() {
+ return fileContentsId(this.file);
+ },
},
watch: {
'file.id': {
@@ -293,7 +300,7 @@ export default {
},
handleToggle({ viaUserInteraction = false } = {}) {
const collapsingNow = !this.isCollapsed;
- const contentElement = this.$el.querySelector(`#diff-content-${this.file.file_hash}`);
+ const contentElement = this.$el.querySelector(`#${fileContentsId(this.file)}`);
this.setFileCollapsedByUser({
filePath: this.file.file_path,
@@ -386,6 +393,7 @@ export default {
'comments-disabled': Boolean(file.brokenSymlink),
'has-body': showBody,
'is-virtual-scrolling': isVirtualScrollingEnabled,
+ 'pinned-file': isPinnedFile,
}"
:data-path="file.new_path"
class="diff-file file-holder gl-border-none gl-mb-0! gl-pb-5"
@@ -400,6 +408,7 @@ export default {
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
:show-local-file-reviews="showLocalFileReviews"
+ :pinned="isPinnedFile"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
@toggleFile="handleToggle({ viaUserInteraction: true })"
@@ -428,7 +437,7 @@ export default {
</div>
<template v-else>
<div
- :id="`diff-content-${file.file_hash}`"
+ :id="fileId"
:class="hasBodyClasses.contentByHash"
class="diff-content"
data-testid="content-area"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index e45fd508a5b..97db0fc1c24 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -22,6 +22,7 @@ import { __, s__, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { fileContentsId, pinnedFileHref } from '~/diffs/components/diff_row_utils';
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
import { collapsedType, isCollapsed } from '../utils/diff_file';
@@ -102,6 +103,11 @@ export default {
required: false,
default: false,
},
+ pinned: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
idState() {
return {
@@ -113,9 +119,8 @@ export default {
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
...mapGetters(['getNoteableData']),
diffContentIDSelector() {
- return `#diff-content-${this.diffFile.file_hash}`;
+ return `${pinnedFileHref(this.diffFile)}#${fileContentsId(this.diffFile)}`;
},
-
titleLink() {
if (this.diffFile.submodule) {
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
@@ -222,6 +227,7 @@ export default {
'setFileForcedOpen',
'setGenerateTestFilePath',
'toggleFileCommentForm',
+ 'unpinFile',
]),
handleToggleFile() {
this.setFileForcedOpen({
@@ -295,7 +301,19 @@ export default {
>
<div class="file-header-content">
<gl-button
- v-if="collapsible"
+ v-if="pinned"
+ v-gl-tooltip.hover.focus
+ :title="__('Unpin the file')"
+ :aria-label="__('Unpin the file')"
+ icon="thumbtack"
+ size="small"
+ class="btn-icon gl-mr-2"
+ category="tertiary"
+ data-testid="unpin-button"
+ @click="unpinFile"
+ />
+ <gl-button
+ v-else-if="collapsible"
ref="collapseButton"
class="gl-mr-2"
category="tertiary"
@@ -305,10 +323,10 @@ export default {
@click.stop="handleToggleFile"
/>
<a
- ref="titleWrapper"
:v-once="!viewDiffsFileByFile"
class="gl-mr-2 gl-text-decoration-none! gl-word-break-all"
:href="titleLink"
+ data-testid="file-title"
@click="handleFileNameClick"
>
<span v-if="isFileRenamed">
@@ -354,7 +372,7 @@ export default {
<small
v-if="isModeChanged"
ref="fileMode"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
class="mr-1"
:title="$options.i18n.fileModeTooltip"
>
@@ -377,7 +395,7 @@ export default {
/>
<gl-form-checkbox
v-if="isReviewable && showLocalFileReviews"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
data-testid="fileReviewCheckbox"
class="gl-mr-5 gl-mb-n3 gl-display-flex gl-align-items-center"
:title="$options.i18n.fileReviewTooltip"
@@ -388,7 +406,7 @@ export default {
</gl-form-checkbox>
<gl-button
v-if="showCommentButton"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
:title="__('Comment on this file')"
:aria-label="__('Comment on this file')"
icon="comment"
@@ -402,7 +420,7 @@ export default {
<gl-button
v-if="diffFile.external_url"
ref="externalLink"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
:href="diffFile.external_url"
:title="externalUrlLabel"
:aria-label="externalUrlLabel"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 3dad7a1a8e4..a9da77104d4 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -292,7 +292,7 @@ export default {
v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR"
:data-linenumber="props.line.left.old_line"
:href="props.line.lineHrefOld"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
<component
@@ -318,7 +318,7 @@ export default {
v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR"
:data-linenumber="props.line.left.new_line"
:href="props.line.lineHrefOld"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
</div>
@@ -446,7 +446,7 @@ export default {
v-if="props.line.right.new_line"
:data-linenumber="props.line.right.new_line"
:href="props.line.lineHrefNew"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
<component
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index a489c96b0c9..5c62e0179ac 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -33,7 +33,19 @@ export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) =
export const hasDiscussions = (line) => line?.discussions?.length > 0;
-export const lineHref = (line) => `#${line?.line_code || ''}`;
+export const pinnedFileHref = (diffFile) => {
+ if (!window?.gon?.features?.pinnedFile) return '';
+ return `?pin=${diffFile.file_hash}`;
+};
+
+export const lineHref = (line, content) => {
+ if (!line || !line.line_code) return '';
+ return `${pinnedFileHref(content.diffFile)}#${line.line_code}`;
+};
+
+export const fileContentsId = (diffFile) => {
+ return `diff-content-${diffFile.file_hash}`;
+};
export const lineCode = (line) => {
if (!line) return undefined;
@@ -179,8 +191,8 @@ export const mapParallel = (content) => (line) => {
isContextLineRight: isContextLine(right?.type),
hasDiscussionsLeft: hasDiscussions(left),
hasDiscussionsRight: hasDiscussions(right),
- lineHrefOld: lineHref(left),
- lineHrefNew: lineHref(right),
+ lineHrefOld: lineHref(left, content),
+ lineHrefNew: lineHref(right, content),
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 07984beb709..ab21391b364 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -34,7 +34,7 @@ export default {
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
- ...mapGetters('diffs', ['allBlobs']),
+ ...mapGetters('diffs', ['allBlobs', 'pinnedFile']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@@ -71,21 +71,59 @@ export default {
// out: [{ path: 'a', tree: [{ path: 'b' }] }, { path: 'b' }, { path: 'c' }]
flatFilteredTreeList() {
const result = [];
- const createFlatten = (level) => (item) => {
+ const createFlatten = (level, hidden) => (item) => {
result.push({
...item,
+ hidden,
level: item.isHeader ? 0 : level,
key: item.key || item.path,
});
- if (item.opened || item.isHeader) {
- item.tree.forEach(createFlatten(level + 1));
- }
+ const isHidden = hidden || (item.type === 'tree' && !item.opened);
+ item.tree.forEach(createFlatten(level + 1, isHidden));
};
this.filteredTreeList.forEach(createFlatten(0));
return result;
},
+ flatListWithPinnedFile() {
+ const result = [...this.flatFilteredTreeList];
+ const pinnedIndex = result.findIndex((item) => item.path === this.pinnedFile.file_path);
+ const [pinnedItem] = result.splice(pinnedIndex, 1);
+
+ if (pinnedItem.parentPath === '/')
+ return [{ ...pinnedItem, level: 0, pinned: true, hidden: false }, ...result];
+
+ // remove detached folder from the tree
+ const next = result[pinnedIndex];
+ const prev = result[pinnedIndex - 1];
+ const hasContainingFolder =
+ prev && prev.type === 'tree' && prev.level === pinnedItem.level - 1;
+ const hasSibling = next && next.type !== 'tree' && next.level === pinnedItem.level;
+ if (hasContainingFolder && !hasSibling) {
+ // folder tree is always condensed so we only need to remove the parent folder
+ result.splice(pinnedIndex - 1, 1);
+ }
+
+ return [
+ {
+ level: 0,
+ key: 'pinned-path',
+ isHeader: true,
+ opened: true,
+ path: pinnedItem.parentPath,
+ type: 'tree',
+ hidden: false,
+ },
+ { ...pinnedItem, level: 1, pinned: true, hidden: false },
+ ...result,
+ ];
+ },
+ treeList() {
+ const list = this.pinnedFile ? this.flatListWithPinnedFile : this.flatFilteredTreeList;
+ if (this.search) return list;
+ return list.filter((item) => !item.hidden);
+ },
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
@@ -125,13 +163,13 @@ export default {
</button>
</div>
</div>
- <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
+ <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="treeList.length">
<template #default="{ scrollerHeight, rowHeight }">
<div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
<recycle-scroller
- v-if="flatFilteredTreeList.length"
+ v-if="treeList.length"
:style="{ height: `${scrollerHeight}px` }"
- :items="flatFilteredTreeList"
+ :items="treeList"
:item-size="rowHeight"
:buffer="100"
key-field="key"
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 15e4225f062..b219b5499c9 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -93,6 +93,7 @@ export default function initDiffsApp(store = notesStore) {
helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
+ pinnedFileUrl: dataset.pinnedFileUrl,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 1c0e20183e2..061ea7a421e 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -54,7 +54,6 @@ import {
} from '../constants';
import {
DISCUSSION_SINGLE_DIFF_FAILED,
- LOAD_SINGLE_DIFF_FAILED,
BUILDING_YOUR_MR,
SOMETHING_WENT_WRONG,
ERROR_LOADING_FULL_DIFF,
@@ -210,7 +209,7 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
}
};
-export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
+export const fetchDiffFilesBatch = ({ commit, state, dispatch }, pinnedFileLoading = false) => {
let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
@@ -224,8 +223,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
let totalLoaded = 0;
let scrolledVirtualScroller = hash === '';
- commit(types.SET_BATCH_LOADING_STATE, 'loading');
- commit(types.SET_RETRIEVING_BATCHES, true);
+ if (!pinnedFileLoading) {
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+ }
eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START);
const getBatch = (page = startPage) =>
@@ -237,7 +238,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffFiles });
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
- if (!scrolledVirtualScroller) {
+ if (!scrolledVirtualScroller && !pinnedFileLoading) {
const index = state.diffFiles.findIndex(
(f) =>
f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash),
@@ -301,9 +302,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
return null;
})
- .catch(() => {
+ .catch((error) => {
commit(types.SET_RETRIEVING_BATCHES, false);
commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
});
return getBatch();
@@ -384,7 +386,11 @@ export const fetchCoverageFiles = ({ commit, state }) => {
coveragePoll.makeRequest();
};
-export const setHighlightedRow = ({ commit }, lineCode) => {
+export const setHighlightedRow = ({ commit }, { lineCode, event }) => {
+ if (event && event.target.href) {
+ event.preventDefault();
+ window.history.replaceState(null, undefined, event.target.href);
+ }
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -657,6 +663,8 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
} else {
if (!state.treeEntries[path]) return;
+ dispatch('unpinFile');
+
const { fileHash } = state.treeEntries[path];
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -667,11 +675,7 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
scrollToElement('.diff-files-holder', { duration: 0 });
if (!getters.isTreePathLoaded(path)) {
- dispatch('fetchFileByFile').catch(() => {
- createAlert({
- message: LOAD_SINGLE_DIFF_FAILED,
- });
- });
+ dispatch('fetchFileByFile');
}
}
};
@@ -995,6 +999,8 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n
};
export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => {
+ dispatch('unpinFile');
+
const { fileHash } = getters.flatBlobsList[index];
document.location.hash = fileHash;
@@ -1055,3 +1061,50 @@ export const toggleFileCommentForm = ({ state, commit }, filePath) => {
export const addDraftToFile = ({ commit }, { filePath, draft }) =>
commit(types.ADD_DRAFT_TO_FILE, { filePath, draft });
+
+export const fetchPinnedFile = ({ state, commit }, pinnedFileUrl) => {
+ const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+
+ return axios
+ .get(pinnedFileUrl)
+ .then(({ data: diffData }) => {
+ const [{ file_hash }] = diffData.diff_files;
+
+ // we must store pinned file in the `diffs`, otherwise collapsing and commenting on a file won't work
+ // once the same file arrives in a file batch we must only update its' position
+ // we also must not update file's position since it's loaded out of order
+ commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files, updatePosition: false });
+ commit(types.SET_PINNED_FILE_HASH, file_hash);
+
+ if (!isNoteLink && !state.currentDiffFileId) {
+ commit(types.SET_CURRENT_DIFF_FILE, file_hash);
+ }
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loaded');
+
+ setTimeout(() => {
+ handleLocationHash();
+ });
+
+ eventHub.$emit('diffFilesModified');
+ })
+ .catch((error) => {
+ commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
+ })
+ .finally(() => {
+ commit(types.SET_RETRIEVING_BATCHES, false);
+ });
+};
+
+export const unpinFile = ({ getters, commit }) => {
+ if (!getters.pinnedFile) return;
+ commit(types.SET_PINNED_FILE_HASH, null);
+ const newUrl = new URL(window.location);
+ newUrl.searchParams.delete('pin');
+ newUrl.hash = '';
+ window.history.replaceState(null, undefined, newUrl);
+};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index bfafb4d281d..f15c3d1693c 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -201,3 +201,18 @@ export const isVirtualScrollingEnabled = (state) => {
export const isBatchLoading = (state) => state.batchLoadingState === 'loading';
export const isBatchLoadingError = (state) => state.batchLoadingState === 'error';
+
+export const diffFiles = (state, getters) => {
+ const { pinnedFile } = getters;
+ if (pinnedFile) {
+ const diffs = state.diffFiles.slice(0);
+ diffs.splice(diffs.indexOf(pinnedFile), 1);
+ return [pinnedFile, ...diffs];
+ }
+ return state.diffFiles;
+};
+
+export const pinnedFile = (state) => {
+ if (!state.pinnedFileHash) return null;
+ return state.diffFiles.find((file) => file.file_hash === state.pinnedFileHash);
+};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d5e1a05f4a5..3b6408e6d78 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -37,4 +37,5 @@ export default () => ({
mrReviews: {},
latestDiff: true,
disableVirtualScroller: false,
+ pinnedFileHash: null,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index b155804c70c..7b19f96b151 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -6,6 +6,7 @@ export const SET_RETRIEVING_BATCHES = 'SET_RETRIEVING_BATCHES';
export const SET_DIFF_METADATA = 'SET_DIFF_METADATA';
export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH';
export const SET_DIFF_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_TREE_ENTRY = 'SET_DIFF_TREE_ENTRY';
export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS';
@@ -23,6 +24,7 @@ export const TREE_ENTRY_DIFF_LOADING = 'TREE_ENTRY_DIFF_LOADING';
export const SET_SHOW_TREE_LIST = 'SET_SHOW_TREE_LIST';
export const SET_CURRENT_DIFF_FILE = 'SET_CURRENT_DIFF_FILE';
export const SET_DIFF_FILE_VIEWED = 'SET_DIFF_FILE_VIEWED';
+export const SET_PINNED_FILE_HASH = 'SET_PINNED_FILE_HASH';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index bc5ed3c40df..3c750830baa 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -81,15 +81,27 @@ export default {
});
},
- [types.SET_DIFF_DATA_BATCH](state, data) {
+ [types.SET_DIFF_DATA_BATCH](state, { diff_files: diffFiles, updatePosition = true }) {
Object.assign(state, {
diffFiles: prepareDiffData({
- diff: data,
+ diff: { diff_files: diffFiles },
priorFiles: state.diffFiles,
+ // when a pinned file is added to diffs its position may be incorrect since it's loaded out of order
+ // we need to ensure when we load it in batched request it updates it position
+ updatePosition,
}),
treeEntries: markTreeEntriesLoaded({
priorEntries: state.treeEntries,
- loadedFiles: data.diff_files,
+ loadedFiles: diffFiles,
+ }),
+ });
+ },
+
+ [types.SET_DIFF_TREE_ENTRY](state, diffFile) {
+ Object.assign(state, {
+ treeEntries: markTreeEntriesLoaded({
+ priorEntries: state.treeEntries,
+ loadedFiles: [diffFile],
}),
});
},
@@ -404,4 +416,7 @@ export default {
file?.drafts.push(draft);
},
+ [types.SET_PINNED_FILE_HASH](state, fileHash) {
+ state.pinnedFileHash = fileHash;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index fb467a606b9..ad8cacf8504 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -395,9 +395,15 @@ function finalizeDiffFile(file) {
return file;
}
-function deduplicateFilesList(files) {
+function deduplicateFilesList(files, updatePosition) {
const dedupedFiles = files.reduce((newList, file) => {
const id = diffFileUniqueId(file);
+ if (updatePosition && id in newList) {
+ // Object.values preserves key order but doesn't update order when writing to the same key
+ // In order to update position of the item we have to delete it first and then add it back
+ // eslint-disable-next-line no-param-reassign
+ delete newList[id];
+ }
return {
...newList,
@@ -408,14 +414,20 @@ function deduplicateFilesList(files) {
return Object.values(dedupedFiles);
}
-export function prepareDiffData({ diff, priorFiles = [], meta = false }) {
- const cleanedFiles = (diff.diff_files || [])
- .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta, index }))
- .map(ensureBasicDiffFileLines)
- .map(prepareDiffFileLines)
- .map((file) => finalizeDiffFile(file));
+export function prepareDiffData({ diff, priorFiles = [], meta = false, updatePosition = false }) {
+ const transformersChain = [
+ (file, index, allFiles) => prepareRawDiffFile({ file, index, allFiles, meta }),
+ ensureBasicDiffFileLines,
+ prepareDiffFileLines,
+ finalizeDiffFile,
+ ];
+ const cleanedFiles = (diff.diff_files || []).map((file, index, allFiles) => {
+ return transformersChain.reduce((fileResult, transformer) => {
+ return transformer(fileResult, index, allFiles);
+ }, file);
+ });
- return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
+ return deduplicateFilesList([...priorFiles, ...cleanedFiles], updatePosition);
}
export function getDiffPositionByLineCode(diffFiles) {
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 3a08e3e546f..6e347a3c95b 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -20,6 +20,7 @@ import {
VISIBILITY_LEVELS_STRING_TO_INTEGER,
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
import { ITEM_TYPE, ACTIVE_TAB_SHARED } from '../constants';
@@ -105,7 +106,8 @@ export default {
return VISIBILITY_TYPE_ICON[this.group.visibility];
},
visibilityTooltip() {
- return GROUP_VISIBILITY_TYPE[this.group.visibility];
+ if (this.isGroup) return GROUP_VISIBILITY_TYPE[this.group.visibility];
+ return PROJECT_VISIBILITY_TYPE[this.group.visibility];
},
microdata() {
return this.group.microdata || {};
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
index 32f05d0e298..d9177778803 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -40,6 +40,9 @@ export default {
totalItems() {
return this.packageProtectionRules.length;
},
+ isLoadingPackageProtectionRules() {
+ return this.$apollo.queries.packageProtectionRules.loading;
+ },
},
apollo: {
packageProtectionRules: {
@@ -99,6 +102,7 @@ export default {
show-empty
stacked="md"
class="mb-3"
+ :busy="isLoadingPackageProtectionRules"
>
<template #table-busy>
<gl-loading-icon size="sm" class="gl-my-5" />
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index df99aac6b9e..89ae2a82c6d 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -25,10 +25,11 @@ export const getUsers = (query, states) => {
});
};
-export const getGroups = () => {
+export const getGroups = ({ withProjectAccess = false }) => {
return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
params: {
project_id: gon.current_project_id,
+ with_project_access: withProjectAccess,
},
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 2dd7633e2c8..c863973cd2e 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -94,6 +94,11 @@ export default {
required: false,
default: true,
},
+ groupsWithProjectAccess: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -229,7 +234,9 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length
+ ? Promise.resolve({ data: this.groups })
+ : getGroups({ withProjectAccess: this.groupsWithProjectAccess }),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 49c55efca7b..612f801300e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -83,6 +83,7 @@ export default class ProtectedBranchCreate {
block: true,
accessLevel,
accessLevelsData,
+ groupsWithProjectAccess: true,
testId,
});
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 66da3de516a..67ae33e1fc8 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -97,6 +97,7 @@ export default class ProtectedBranchEdit {
block: true,
accessLevel,
accessLevelsData,
+ groupsWithProjectAccess: true,
testId,
});
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index b3754cecce4..e1cd069fd92 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -40,6 +40,7 @@ export default class ProtectedTagCreate {
hasLicense: this.hasLicense,
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
+ groupsWithProjectAccess: true,
searchEnabled: dropdownEl.dataset.filter !== undefined,
testId: 'allowed-to-create-dropdown',
});
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
index 7fe1dc9c01a..d5ec88c171c 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.vue
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -107,6 +107,7 @@ export default {
:access-levels-data="accessLevelsData"
:preselected-items="selected"
:search-enabled="searchEnabled"
+ groups-with-project-access
:block="true"
@hidden="updatePermissions"
/>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 6ac75230d88..3419e6eb1b6 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTruncate } from '@gitlab/ui';
+import { GlTruncate, GlIcon } from '@gitlab/ui';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
@@ -10,6 +10,7 @@ export default {
FileHeader,
FileIcon,
GlTruncate,
+ GlIcon,
},
props: {
file: {
@@ -141,6 +142,7 @@ export default {
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
+ <gl-icon v-if="file.pinned" name="thumbtack" :size="16" />
<file-icon
class="file-row-icon"
:class="{ 'text-secondary': file.type === 'tree' }"
diff --git a/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
index 5d86f90880d..5f197066cb5 100644
--- a/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncate } from '~/lib/utils/text_utility';
import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
@@ -12,6 +13,7 @@ export default {
GlBadge,
GlIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
user: {
type: Object,
@@ -65,7 +67,12 @@ export default {
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
<gl-icon v-gl-tooltip="userNoteShort" name="document" />
</div>
- <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
+ <div
+ v-for="(badge, idx) in user.badges"
+ :key="idx"
+ class="gl-p-1"
+ :class="{ 'gl-pb-0': glFeatures.simplifiedBadges }"
+ >
<gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
badge.text
}}</gl-badge>
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 3f1d742ca14..7c3684f7c2e 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -12,3 +12,56 @@
color: $green;
}
}
+
+// FF :simplified_badges
+//
+// Temporarily override badge styles
+// globally
+//
+// Once verified we will update the
+// badge component in GitLab UI
+// refactor GitLab and remove this
+// custom code
+//
+// see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3307
+.ff-simplified-badges-enabled {
+ // These changes will be moved to
+ // GitLab UI's badge component
+ .gl-badge,
+ .gl-badge.sm,
+ .gl-badge.md,
+ .gl-badge.lg {
+ @include gl-font-sm;
+ padding-block: $gl-spacing-scale-1;
+ padding-inline: calc(#{$gl-spacing-scale-3} - 2px);
+
+ > .gl-icon {
+ @include gl-ml-0;
+ }
+ }
+
+ // These changes will be moved to
+ // GitLab UI's button component
+ .gl-button .gl-badge {
+ @include gl-py-0;
+ }
+
+ // These changes will be moved to
+ // app/assets/stylesheets/framework/super_sidebar.scss
+ .super-sidebar-nav-item .gl-badge {
+ vertical-align: 2px;
+ }
+
+ // These changes will be moved to
+ // GitLab UI's tab component
+ .gl-tab-nav-item .gl-badge {
+ margin-block: -2px;
+ }
+
+ // Temporarily needed because of the
+ // speciality this FF adds
+ // the utility class gets overriden
+ .gl-badge.ci-icon {
+ @include gl-p-2;
+ }
+}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index c9ef9349c36..497a8a08a6f 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -944,3 +944,12 @@ table.code {
left: -2px !important;
}
}
+
+.diff-file.pinned-file .file-title {
+ background-color: $blue-50;
+ border-color: $blue-200;
+}
+
+.diff-file.pinned-file .diff-content {
+ border-color: $blue-200;
+}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index ee78d5a8c35..3a0618c0d40 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -9,6 +9,10 @@ class Admin::UsersController < Admin::ApplicationController
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :set_shared_view_parameters, only: [:show, :projects, :keys]
+ before_action only: [:index] do
+ push_frontend_feature_flag(:simplified_badges)
+ end
+
feature_category :user_management
PAGINATION_WITH_COUNT_LIMIT = 1000
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 09ba6e40efd..6cb00fea922 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -47,6 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_request_changes, current_user)
push_frontend_feature_flag(:merge_blocked_component, current_user)
push_frontend_feature_flag(:mention_autocomplete_backend_filtering, project)
+ push_frontend_feature_flag(:pinned_file, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions]
@@ -449,6 +450,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@update_current_user_path = expose_path(api_v4_user_preferences_path)
@endpoint_metadata_url = endpoint_metadata_url(@project, @merge_request)
@endpoint_diff_batch_url = endpoint_diff_batch_url(@project, @merge_request)
+ if params[:pin] && Feature.enabled?(:pinned_file)
+ @pinned_file_url = diff_by_file_hash_namespace_project_merge_request_path(
+ format: 'json',
+ id: merge_request.iid,
+ namespace_id: project&.namespace.to_param,
+ project_id: project&.path,
+ file_hash: params[:pin]
+ )
+ end
if merge_request.diffs_batch_cache_with_max_age?
@diffs_batch_cache_key = @merge_request.merge_head_diff&.patch_id_sha
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 767838df076..522fb134002 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -204,7 +204,8 @@ module MergeRequestsHelper
is_forked: project.forked?.to_s,
new_comment_template_path: profile_comment_templates_path,
iid: merge_request.iid,
- per_page: DIFF_BATCH_ENDPOINT_PER_PAGE
+ per_page: DIFF_BATCH_ENDPOINT_PER_PAGE,
+ pinned_file_url: @pinned_file_url
}
end
diff --git a/app/services/system_notes/time_tracking_service.rb b/app/services/system_notes/time_tracking_service.rb
index f9084ed67d3..6ebf1215a25 100644
--- a/app/services/system_notes/time_tracking_service.rb
+++ b/app/services/system_notes/time_tracking_service.rb
@@ -43,18 +43,11 @@ module SystemNotes
#
# Returns the created Note object
def change_time_estimate
- parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
- body = if noteable.time_estimate == 0
- "removed time estimate"
- else
- "changed time estimate to #{parsed_time}"
- end
-
if noteable.is_a?(Issue)
issue_activity_counter.track_issue_time_estimate_changed_action(author: author, project: project)
end
- create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
+ create_note(NoteSummary.new(noteable, project, author, time_estimate_system_note, action: 'time_tracking'))
end
# Called when the spent time of a Noteable is changed
@@ -160,5 +153,19 @@ module SystemNotes
def work_item_activity_counter
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter
end
+
+ def time_estimate_system_note
+ parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
+ previous_estimate = noteable.previous_changes['time_estimate']&.at(0) || 0
+ parsed_previous_restimate = Gitlab::TimeTrackingFormatter.output(previous_estimate)
+
+ if previous_estimate == 0
+ "added time estimate of #{parsed_time}"
+ elsif noteable.time_estimate == 0
+ "removed time estimate of #{parsed_previous_restimate}"
+ else
+ "changed time estimate to #{parsed_time} from #{parsed_previous_restimate}"
+ end
+ end
end
end
diff --git a/app/views/devise/shared/_footer.html.haml b/app/views/devise/shared/_footer.html.haml
index c35e43b909e..44f34e3f342 100644
--- a/app/views/devise/shared/_footer.html.haml
+++ b/app/views/devise/shared/_footer.html.haml
@@ -7,5 +7,8 @@
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= link_to _("Community forum"), ApplicationHelper.community_forum, target: '_blank', class: 'text-nowrap', rel: 'noopener noreferrer'
+ - if one_trust_enabled?
+ = render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, button_options: { class: 'ot-sdk-show-settings' }) do
+ = _("Cookie Preferences")
= render 'devise/shared/language_switcher'
= footer_message
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 71c5e2d24a7..b9257bcedc9 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -2,11 +2,12 @@
- page_classes = [user_application_theme, page_classes.flatten.compact]
- body_classes = [user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
- ff_simplified_labels_enabled = Feature.enabled?(:simplified_labels) ? 'ff-simplified-labels-enabled' : ''
+- ff_simplified_badges_class = Feature.enabled?(:simplified_badges) ? 'ff-simplified-badges-enabled' : ''
!!! 5
%html{ lang: I18n.locale, class: page_classes }
= render "layouts/head"
- %body{ class: [body_classes, ff_simplified_labels_enabled], data: body_data }
+ %body{ class: [body_classes, ff_simplified_labels_enabled, ff_simplified_badges_class], data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
= render "layouts/init_client_detection_flags"
= render "layouts/visual_review" if review_apps_enabled?
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index 03a1f2f3179..af8ad22fa50 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -16,6 +16,7 @@
- add_page_specific_style 'page_bundles/ci_status'
- add_page_startup_api_call @endpoint_metadata_url
+- add_page_startup_api_call @pinned_file_url if @pinned_file_url
- if mr_action == 'diffs' && !@file_by_file_default
- add_page_startup_api_call @endpoint_diff_batch_url
diff --git a/config/feature_flags/development/group_analytics_dashboards.yml b/config/feature_flags/development/group_analytics_dashboards.yml
index 55001b99452..6609b4d2c7e 100644
--- a/config/feature_flags/development/group_analytics_dashboards.yml
+++ b/config/feature_flags/development/group_analytics_dashboards.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416970
milestone: '16.2'
type: development
group: group::optimize
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/pinned_file.yml b/config/feature_flags/development/pinned_file.yml
new file mode 100644
index 00000000000..712a35fd790
--- /dev/null
+++ b/config/feature_flags/development/pinned_file.yml
@@ -0,0 +1,8 @@
+---
+name: pinned_file
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137544
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433250
+milestone: '16.9'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/simplified_badges.yml b/config/feature_flags/development/simplified_badges.yml
new file mode 100644
index 00000000000..95f9b00d306
--- /dev/null
+++ b/config/feature_flags/development/simplified_badges.yml
@@ -0,0 +1,8 @@
+---
+name: simplified_badges
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140231
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437861
+milestone: '16.8'
+type: development
+group: group::ux paper cuts
+default_enabled: false
diff --git a/db/migrate/20240111134328_routing_table_prepare_async_constraint_for_pipeline_variables.rb b/db/migrate/20240111134328_routing_table_prepare_async_constraint_for_pipeline_variables.rb
new file mode 100644
index 00000000000..4cf3d9c1721
--- /dev/null
+++ b/db/migrate/20240111134328_routing_table_prepare_async_constraint_for_pipeline_variables.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class RoutingTablePrepareAsyncConstraintForPipelineVariables < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers
+
+ milestone '16.8'
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_pipeline_variables
+ PARENT_TABLE_NAME = :p_ci_pipeline_variables
+ FIRST_PARTITION = [100, 101]
+ PARTITION_COLUMN = :partition_id
+
+ def up
+ prepare_constraint_for_list_partitioning(
+ table_name: TABLE_NAME,
+ partitioning_column: PARTITION_COLUMN,
+ parent_table_name: PARENT_TABLE_NAME,
+ initial_partitioning_value: FIRST_PARTITION,
+ async: true
+ )
+ end
+
+ def down
+ revert_preparing_constraint_for_list_partitioning(
+ table_name: TABLE_NAME,
+ partitioning_column: PARTITION_COLUMN,
+ parent_table_name: PARENT_TABLE_NAME,
+ initial_partitioning_value: FIRST_PARTITION
+ )
+ end
+end
diff --git a/db/schema_migrations/20240111134328 b/db/schema_migrations/20240111134328
new file mode 100644
index 00000000000..268782c5acc
--- /dev/null
+++ b/db/schema_migrations/20240111134328
@@ -0,0 +1 @@
+0fc191808377dfe56ece157bc9d44899ce8635260e894972f2d058822707b080 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index deca0111644..913c244b820 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -30071,6 +30071,9 @@ ALTER TABLE ONLY pages_domain_acme_orders
ALTER TABLE ONLY pages_domains
ADD CONSTRAINT pages_domains_pkey PRIMARY KEY (id);
+ALTER TABLE ci_pipeline_variables
+ ADD CONSTRAINT partitioning_constraint CHECK ((partition_id = ANY (ARRAY[(100)::bigint, (101)::bigint]))) NOT VALID;
+
ALTER TABLE ONLY path_locks
ADD CONSTRAINT path_locks_pkey PRIMARY KEY (id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e433277d560..767ceb154a7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -17235,8 +17235,8 @@ Represents a product analytics dashboard visualization.
| <a id="customizablepermissionavailablefor"></a>`availableFor` | [`[String!]!`](#string) | Objects the permission is available for. |
| <a id="customizablepermissiondescription"></a>`description` | [`String`](#string) | Description of the permission. |
| <a id="customizablepermissionname"></a>`name` | [`String!`](#string) | Localized name of the permission. |
-| <a id="customizablepermissionrequirement"></a>`requirement` | [`String`](#string) | Requirement of the permission. |
-| <a id="customizablepermissionvalue"></a>`value` | [`String!`](#string) | Value of the permission. |
+| <a id="customizablepermissionrequirement"></a>`requirement` | [`MemberRolePermission`](#memberrolepermission) | Requirement of the permission. |
+| <a id="customizablepermissionvalue"></a>`value` | [`MemberRolePermission!`](#memberrolepermission) | Value of the permission. |
### `DastPreScanVerification`
@@ -21487,7 +21487,7 @@ Represents a member role.
| ---- | ---- | ----------- |
| <a id="memberrolebaseaccesslevel"></a>`baseAccessLevel` **{warning-solid}** | [`AccessLevel!`](#accesslevel) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Base access level for the custom role. |
| <a id="memberroledescription"></a>`description` | [`String`](#string) | Description of the member role. |
-| <a id="memberroleenabledpermissions"></a>`enabledPermissions` **{warning-solid}** | [`[MemberRolePermission!]`](#memberrolepermission) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Array of all permissions enabled for the custom role. |
+| <a id="memberroleenabledpermissions"></a>`enabledPermissions` **{warning-solid}** | [`CustomizablePermissionConnection`](#customizablepermissionconnection) | **Introduced** in 16.5. This feature is an Experiment. It can be changed or removed at any time. Array of all permissions enabled for the custom role. |
| <a id="memberroleid"></a>`id` | [`MemberRoleID!`](#memberroleid) | ID of the member role. |
| <a id="memberrolememberscount"></a>`membersCount` **{warning-solid}** | [`Int!`](#int) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Total number of members with the custom role. |
| <a id="memberrolename"></a>`name` | [`String!`](#string) | Name of the member role. |
diff --git a/doc/ci/jobs/job_artifacts_troubleshooting.md b/doc/ci/jobs/job_artifacts_troubleshooting.md
index 0b7777d2d82..470c1bf4b55 100644
--- a/doc/ci/jobs/job_artifacts_troubleshooting.md
+++ b/doc/ci/jobs/job_artifacts_troubleshooting.md
@@ -155,3 +155,26 @@ To troubleshoot this error, verify that:
parent-child pipeline hierarchy.
- The `pipeline` and `job` combination exists and resolves to an existing pipeline.
- `dependency-job` has run and finished successfully.
+
+## Jobs show `UnlockPipelinesInQueueWorker` after an upgrade
+
+Jobs might stall and show an error that states `UnlockPipelinesInQueueWorker`.
+
+This issue occurs after an upgrade.
+
+The workaround is to enable the `ci_unlock_pipelines_extra_low` feature flag.
+To toggle feature flags, you must be an administrator.
+
+On GitLab SaaS:
+
+- Run the following [ChatOps](../chatops/index.md) command:
+
+ ```ruby
+ /chatops run feature set ci_unlock_pipelines_extra_low true
+ ```
+
+On GitLab self-managed:
+
+- [Enable the feature flag](../../administration/feature_flags.md) named `ci_unlock_pipelines_extra_low`.
+
+For more information see the comment in [merge request 140318](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140318#note_1718600424).
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 447e0bd93da..156874d196d 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -1361,15 +1361,7 @@ job:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223273) in GitLab 13.8 [with a flag](../../user/feature_flags.md) named `non_public_artifacts`, disabled by default.
> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/322454) in GitLab 15.10. Artifacts created with `artifacts:public` before 15.10 are not guaranteed to remain private after this update.
-> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/294503) in GitLab 16.7. Rolled out and removed a feature flag named `non_public_artifacts`
-
-WARNING:
-On self-managed GitLab, by default this feature is not available. To make it available,
-an administrator can [enable the feature flag](../../administration/feature_flags.md) named `non_public_artifacts`. On
-GitLab.com, this feature is not available. Due to [issue 413822](https://gitlab.com/gitlab-org/gitlab/-/issues/413822),
-the keyword can be used when the feature flag is disabled, but the feature does not work.
-Do not attempt to use this feature when the feature flag is disabled, and always test
-with non-production data first.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/294503) in GitLab 16.7. Feature flag `non_public_artifacts` removed.
Use `artifacts:public` to determine whether the job artifacts should be
publicly available.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index af6a536b2e2..a18b376a1cc 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -243,7 +243,7 @@ by default.
Capitalize names of:
- GitLab [product tiers](https://about.gitlab.com/pricing/). For example,
- GitLab Free and GitLab Ultimate. (Tested in [`BadgeCapitalization.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/BadgeCapitalization.yml).)
+ GitLab Free and GitLab Ultimate.
- Third-party organizations, software, and products. For example, Prometheus,
Kubernetes, Git, and The Linux Foundation.
- Methods or methodologies. For example, Continuous Integration,
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
index 6f48f83e7ca..ae33eeb49f4 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
@@ -50,6 +50,53 @@ It is encouraged to fill out as many of `user`, `namespace` and `project` as pos
If a `project` but no `namespace` is provided, the `project.namespace` is used as the `namespace` for the event.
+#### Controller and API helpers
+
+There is a helper module `ProductAnalyticsTracking` for controllers you can use to track internal events for particular controller actions by calling `#track_internal_event`:
+
+```ruby
+class Projects::PipelinesController < Projects::ApplicationController
+ include ProductAnalyticsTracking
+
+ track_internal_event :charts, name: 'p_analytics_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? }
+
+ def charts
+ ...
+ end
+
+ private
+
+ def should_track_ci_cd_pipelines?
+ params[:chart].blank? || params[:chart] == 'pipelines'
+ end
+end
+```
+
+You need to add these two methods to the controller body, so that the helper can get the current project and namespace for the event:
+
+```ruby
+ private
+
+ def tracking_namespace_source
+ project.namespace
+ end
+
+ def tracking_project_source
+ project
+ end
+```
+
+Also, there is an API helper:
+
+```ruby
+track_event(
+ event_name,
+ user: current_user,
+ namespace_id: namespace_id,
+ project_id: project_id
+)
+```
+
### Frontend tracking
Any frontend tracking call automatically passes the values `user.id`, `namespace.id`, and `project.id` from the current context of the page.
diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md
index df9a9d9c4ad..16d0bfdfa30 100644
--- a/doc/development/pipelines/internals.md
+++ b/doc/development/pipelines/internals.md
@@ -435,6 +435,8 @@ For this scenario, you have to:
- `scripts/merge-simplecov`
- `spec/simplecov_env_core.rb`
- `spec/simplecov_env.rb`
+ - `prepare-as-if-foss-env` for:
+ - `scripts/setup/generate-as-if-foss-env.rb`
Additionally, `scripts/utils.sh` is always downloaded from the API when this pattern is used (this file contains the code for `.fast-no-clone-job`).
diff --git a/doc/integration/diffblue-cover.md b/doc/integration/diffblue-cover.md
deleted file mode 100644
index fb181f8e95d..00000000000
--- a/doc/integration/diffblue-cover.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-redirect_to: 'diffblue_cover.md'
-remove_date: '2024-01-10'
----
-
-This document was moved to [another location](diffblue_cover.md).
-
-<!-- This redirect file can be deleted after <2024-01-10>. -->
-<!-- Redirects that point to other docs in the same project expire in three months. -->
-<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
-<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
diff --git a/doc/user/analytics/analytics_dashboards.md b/doc/user/analytics/analytics_dashboards.md
index 8355a7db5e0..e647dcf170a 100644
--- a/doc/user/analytics/analytics_dashboards.md
+++ b/doc/user/analytics/analytics_dashboards.md
@@ -122,12 +122,11 @@ To view the Value Streams Dashboard as an analytics dashboard for a project:
## View group dashboards
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390542) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `group_analytics_dashboards`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390542) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `group_analytics_dashboards`. Disabled by default.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/416970) in GitLab 16.8.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `group_analytics_dashboards`.
-On GitLab.com, this feature is not available.
-This feature is not ready for production use.
+On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `group_analytics_dashboards`. On GitLab.com, this feature is available.
Prerequisites:
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 66e67f56172..a764d0006a1 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -63,9 +63,9 @@ GitLab in a Kubernetes cluster, you might need a different version of Kubernetes
You can upgrade your
Kubernetes version to a supported version at any time:
-- 1.27 (support ends on July 18, 2024 or when 1.30 becomes supported)
-- 1.26 (support ends on March 21, 2024 or when 1.29 becomes supported)
-- 1.25 (support ends on October 22, 2023 or when 1.28 becomes supported)
+- 1.28 (support ends when GitLab version 17.5 is released or when 1.31 becomes supported)
+- 1.27 (support ends when GitLab version 17.2 is released or when 1.30 becomes supported)
+- 1.26 (support ends when GitLab version 16.10 is released or when 1.29 becomes supported)
GitLab aims to support a new minor Kubernetes version three months after its initial release. GitLab supports at least three production-ready Kubernetes minor
versions at any given time.
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index 8e4af7b0af4..b8caf6d0837 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -224,13 +224,19 @@ For the best experience, the version of the agent installed in your cluster shou
### Update the agent version
+NOTE:
+Instead of using `--reuse-values`, you should specify all needed values.
+If you use `--reuse-values`, you might miss new defaults or use deprecated values.
+To retrieve previous `--set` arguments, use `helm get values <release name>`.
+You can save the values to a file with `helm get values gitlab-agent > agent.yaml`, and pass the file to Helm with `-f`:
+`helm upgrade gitlab-agent gitlab/gitlab-agent -f agent.yaml`. This safely replaces the behavior of `--reuse-values`.
+
To update the agent to the latest version, you can run:
```shell
helm repo update
helm upgrade --install gitlab-agent gitlab/gitlab-agent \
--namespace gitlab-agent \
- --reuse-values
```
To set a specific version, you can override the `image.tag` value. For example, to install version `v14.9.1`, run:
@@ -238,7 +244,6 @@ To set a specific version, you can override the `image.tag` value. For example,
```shell
helm upgrade gitlab-agent gitlab/gitlab-agent \
--namespace gitlab-agent \
- --reuse-values \
--set image.tag=v14.9.1
```
diff --git a/gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/listener.rb b/gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/listener.rb
index 6f4dce9df33..c40d9e90b56 100644
--- a/gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/listener.rb
+++ b/gems/gitlab-rspec_flaky/lib/gitlab/rspec_flaky/listener.rb
@@ -36,6 +36,10 @@ module Gitlab
end
def dump_summary(_)
+ rails_logger_warn(
+ "\n#{flaky_examples.count} known flaky example(s) detected. " \
+ "Writing this to #{Config.flaky_examples_report_path}.\n"
+ )
Report.new(flaky_examples).write(Config.flaky_examples_report_path)
return unless new_flaky_examples.any?
diff --git a/gems/gitlab-rspec_flaky/spec/gitlab/rspec_flaky/listener_spec.rb b/gems/gitlab-rspec_flaky/spec/gitlab/rspec_flaky/listener_spec.rb
index b46044e4521..a830226d88f 100644
--- a/gems/gitlab-rspec_flaky/spec/gitlab/rspec_flaky/listener_spec.rb
+++ b/gems/gitlab-rspec_flaky/spec/gitlab/rspec_flaky/listener_spec.rb
@@ -197,6 +197,8 @@ RSpec.describe Gitlab::RspecFlaky::Listener, :aggregate_failures do
end
describe '#dump_summary' do
+ subject { listener.dump_summary(nil) }
+
let(:listener) { described_class.new(suite_flaky_example_report.to_json) }
let(:new_flaky_rspec_example) { double(new_example_attrs.merge(attempts: 2)) }
let(:already_flaky_rspec_example) { double(already_flaky_example_attrs.merge(attempts: 2)) }
@@ -207,6 +209,39 @@ RSpec.describe Gitlab::RspecFlaky::Listener, :aggregate_failures do
allow(Kernel).to receive(:warn)
end
+ context 'when not flaky tests were found' do
+ it 'prints a message in the console' do
+ allow(Kernel).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ %r{0 known flaky example\(s\) detected\. Writing this to rspec/flaky/report\.json}
+ ).to_stderr
+ end
+ end
+
+ context 'when existing flaky tests were found' do
+ before do
+ listener.example_passed(notification_already_flaky_rspec_example)
+ end
+
+ it 'does write them in the correct report' do
+ report = double
+
+ expect(Gitlab::RspecFlaky::Report).to receive(:new).with(listener.flaky_examples).and_return(report)
+ expect(report).to receive(:write).with(Gitlab::RspecFlaky::Config.flaky_examples_report_path)
+
+ subject
+ end
+
+ it 'prints a message in the console' do
+ allow(Kernel).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ %r{1 known flaky example\(s\) detected\. Writing this to rspec/flaky/report\.json}
+ ).to_stderr
+ end
+ end
+
context 'when a report file path is set by FLAKY_RSPEC_REPORT_PATH' do
it 'delegates the writes to RspecFlaky::Report' do
listener.example_passed(notification_new_flaky_rspec_example)
@@ -222,7 +257,7 @@ RSpec.describe Gitlab::RspecFlaky::Listener, :aggregate_failures do
.to receive(:new).with(listener.__send__(:new_flaky_examples)).and_return(report2)
expect(report2).to receive(:write).with(Gitlab::RspecFlaky::Config.new_flaky_examples_report_path)
- listener.dump_summary(nil)
+ subject
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 97e09795f49..94b433193dd 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -330,7 +330,7 @@ module API
mount ::API::Suggestions
mount ::API::SystemHooks
mount ::API::Tags
- mount ::API::Terraform::Modules::V1::Packages
+ mount ::API::Terraform::Modules::V1::NamespacePackages
mount ::API::Terraform::Modules::V1::ProjectPackages
mount ::API::Terraform::State
mount ::API::Terraform::StateVersion
diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/namespace_packages.rb
index 9e82a849c98..1999fc42aba 100644
--- a/lib/api/terraform/modules/v1/packages.rb
+++ b/lib/api/terraform/modules/v1/namespace_packages.rb
@@ -4,7 +4,7 @@ module API
module Terraform
module Modules
module V1
- class Packages < ::API::Base
+ class NamespacePackages < ::API::Base
include ::API::Helpers::Authentication
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
@@ -29,8 +29,10 @@ module API
end
helpers do
+ include ::Gitlab::Utils::StrongMemoize
+
params :module_name do
- requires :module_name, type: String, desc: "", regexp: API::NO_SLASH_URL_PART_REGEX
+ requires :module_name, type: String, desc: '', regexp: API::NO_SLASH_URL_PART_REGEX
requires :module_system, type: String, regexp: API::NO_SLASH_URL_PART_REGEX
end
@@ -39,10 +41,9 @@ module API
end
def module_namespace
- strong_memoize(:module_namespace) do
- find_namespace(params[:module_namespace])
- end
+ find_namespace(params[:module_namespace])
end
+ strong_memoize_attr :module_namespace
def finder_params
{
@@ -55,26 +56,23 @@ module API
end
def packages
- strong_memoize(:packages) do
- ::Packages::GroupPackagesFinder.new(
- current_user,
- module_namespace,
- finder_params
- ).execute
- end
+ ::Packages::GroupPackagesFinder.new(
+ current_user,
+ module_namespace,
+ finder_params
+ ).execute
end
+ strong_memoize_attr :packages
def package
- strong_memoize(:package) do
- packages.first
- end
+ packages.first
end
+ strong_memoize_attr :package
def package_file
- strong_memoize(:package_file) do
- package.installable_package_files.first
- end
+ package.installable_package_files.first
end
+ strong_memoize_attr :package_file
end
params do
@@ -82,7 +80,8 @@ module API
includes :module_name
end
- namespace 'packages/terraform/modules/v1/:module_namespace/:module_name/:module_system', requirements: TERRAFORM_MODULE_REQUIREMENTS do
+ namespace 'packages/terraform/modules/v1/:module_namespace/:module_name/:module_system',
+ requirements: TERRAFORM_MODULE_REQUIREMENTS do
authenticate_with do |accept|
accept.token_types(:personal_access_token, :deploy_token, :job_token)
.sent_through(:http_bearer_token)
@@ -118,7 +117,9 @@ module API
get 'download' do
latest_version = packages.order_version.last&.version
- render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) if latest_version.nil?
+ if latest_version.nil?
+ render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found)
+ end
download_path = api_v4_packages_terraform_modules_v1_module_version_download_path(
{
@@ -145,7 +146,9 @@ module API
get do
latest_package = packages.order_version.last
- render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) if latest_package&.version.nil?
+ if latest_package&.version.nil?
+ render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found)
+ end
presenter = ::Terraform::ModuleVersionPresenter.new(latest_package, params[:module_system])
present presenter, with: ::API::Entities::Terraform::ModuleVersion
@@ -181,13 +184,18 @@ module API
jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
end
- header 'X-Terraform-Get', module_file_path.sub(%r{module_version/file$}, "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz")
+ header 'X-Terraform-Get',
+ module_file_path.sub(
+ %r{module_version/file$},
+ "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz"
+ )
status :no_content
end
namespace 'file' do
authenticate_with do |accept|
- accept.token_types(:deploy_token_from_jwt, :job_token_from_jwt, :personal_access_token_from_jwt).sent_through(:token_param)
+ accept.token_types(:deploy_token_from_jwt, :job_token_from_jwt, :personal_access_token_from_jwt)
+ .sent_through(:token_param)
end
desc 'Download specific version of a module' do
@@ -200,9 +208,14 @@ module API
tags %w[terraform_registry]
end
get do
- track_package_event('pull_package', :terraform_module, project: package.project, namespace: module_namespace)
-
- present_carrierwave_file!(package_file.file)
+ track_package_event(
+ 'pull_package',
+ :terraform_module,
+ project: package.project,
+ namespace: module_namespace
+ )
+
+ present_package_file!(package_file)
end
end
diff --git a/lib/gitlab/database/partitioning/list/convert_table.rb b/lib/gitlab/database/partitioning/list/convert_table.rb
index 9889d01be76..542a7d0a78d 100644
--- a/lib/gitlab/database/partitioning/list/convert_table.rb
+++ b/lib/gitlab/database/partitioning/list/convert_table.rb
@@ -22,7 +22,7 @@ module Gitlab
@table_name = table_name
@parent_table_name = parent_table_name
@partitioning_column = partitioning_column
- @zero_partition_value = zero_partition_value
+ @zero_partition_value = Array.wrap(zero_partition_value)
end
def prepare_for_partitioning(async: false)
@@ -126,10 +126,11 @@ module Gitlab
.check_constraints
.including_column(partitioning_column)
- check_body = "CHECK ((#{partitioning_column} = #{zero_partition_value}))"
+ array_prefix = "CHECK ((#{partitioning_column} = ANY "
+ single_prefix = "CHECK ((#{partitioning_column} = #{zero_partition_value.join(',')}))"
constraints_on_column.find do |constraint|
- constraint.definition.start_with?(check_body)
+ constraint.definition.start_with?(array_prefix, single_prefix)
end
end
@@ -138,14 +139,14 @@ module Gitlab
raise UnableToPartition, <<~MSG
Table #{table_name} is not ready for partitioning.
- Before partitioning, a check constraint must enforce that (#{partitioning_column} = #{zero_partition_value})
+ Before partitioning, a check constraint must enforce that (#{partitioning_column} IN (#{zero_partition_value.join(',')}))
MSG
end
def add_partitioning_check_constraint(async: false)
return validate_partitioning_constraint_synchronously if partitioning_constraint.present?
- check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}"
+ check_body = "#{partitioning_column} IN (#{zero_partition_value.join(',')})"
# Any constraint name would work. The constraint is found based on its definition before partitioning
migration_context.add_check_constraint(
table_name, check_body, PARTITIONING_CONSTRAINT_NAME,
@@ -214,7 +215,7 @@ module Gitlab
<<~SQL
ALTER TABLE #{quote_table_name(parent_table_name)}
ATTACH PARTITION #{table_name}
- FOR VALUES IN (#{zero_partition_value})
+ FOR VALUES IN (#{zero_partition_value.join(',')})
SQL
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 574b52eb337..d82ab30385e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14031,6 +14031,9 @@ msgstr ""
msgid "Converts work item to %{type}. Widgets not supported in new type are removed."
msgstr ""
+msgid "Cookie Preferences"
+msgstr ""
+
msgid "Cookie domain"
msgstr ""
@@ -14352,6 +14355,9 @@ msgstr ""
msgid "Couldn't assign policy to project or group"
msgstr ""
+msgid "Couldn't fetch the pinned file."
+msgstr ""
+
msgid "Couldn't find event type filters where audit event type(s): %{missing_filters}"
msgstr ""
@@ -52375,6 +52381,9 @@ msgstr ""
msgid "Unlocks the discussion."
msgstr ""
+msgid "Unpin the file"
+msgstr ""
+
msgid "Unreachable"
msgstr ""
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 028a6d37af4..c7ad909e6de 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -211,8 +211,7 @@ module QA
def redirect_to_login_page(address)
Menu.perform(&:sign_out_if_signed_in)
- desired_host = URI(Runtime::Scenario.send("#{address}_address")).host
- Runtime::Browser.visit(address, Page::Main::Login) if desired_host != current_host
+ Runtime::Browser.visit(address, Page::Main::Login)
end
private
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 55741a82862..d04cda240fa 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -244,6 +244,24 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
expect(response).to have_gitlab_http_status(:moved_permanently)
end
end
+
+ context 'when has pinned file' do
+ let(:file) { merge_request.merge_request_diff.diffs.diff_files.first }
+ let(:file_hash) { file.file_hash }
+
+ it 'adds pinned file url' do
+ go(pin: file_hash)
+
+ expect(assigns['pinned_file_url']).to eq(
+ diff_by_file_hash_namespace_project_merge_request_path(
+ format: 'json',
+ id: merge_request.iid,
+ namespace_id: project.namespace.to_param,
+ project_id: project.path,
+ file_hash: file_hash
+ ))
+ end
+ end
end
context 'when user is setting notes filters' do
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
index c4476d01386..adb07d4086d 100644
--- a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -35,8 +35,7 @@ describe('RunnerTypeBadge', () => {
expect(findBadge().classes().sort()).toEqual(
[
...classes,
- 'gl-border',
- 'gl-display-inline-block',
+ 'gl-inset-border-1-gray-400',
'gl-max-w-full',
'gl-text-truncate',
'gl-bg-transparent!',
diff --git a/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap b/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap
new file mode 100644
index 00000000000..605f6335b5c
--- /dev/null
+++ b/spec/frontend/diffs/components/__snapshots__/tree_list_spec.js.snap
@@ -0,0 +1,160 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Diffs tree list component pinned file files in folders pins 1.rb file 1`] = `
+Array [
+ "📁folder/",
+ "──1.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins 2.rb file 1`] = `
+Array [
+ "📁folder/",
+ "──2.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins 3.rb file 1`] = `
+Array [
+ "📁folder/",
+ "──3.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins nested-1.rb file 1`] = `
+Array [
+ "📁folder/sub-folder/",
+ "──nested-1.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins nested-2.rb file 1`] = `
+Array [
+ "📁folder/sub-folder/",
+ "──nested-2.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins nested-3.rb file 1`] = `
+Array [
+ "📁folder/sub-folder/",
+ "──nested-3.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins root-first.rb file 1`] = `
+Array [
+ "root-first.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-last.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins root-last.rb file 1`] = `
+Array [
+ "root-last.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "📁folder-single",
+ "──single.rb",
+ "root-first.rb",
+]
+`;
+
+exports[`Diffs tree list component pinned file files in folders pins single.rb file 1`] = `
+Array [
+ "📁folder-single/",
+ "──single.rb",
+ "📁folder",
+ "──📁sub-folder",
+ "────nested-1.rb",
+ "────nested-2.rb",
+ "────nested-3.rb",
+ "──1.rb",
+ "──2.rb",
+ "──3.rb",
+ "root-first.rb",
+ "root-last.rb",
+]
+`;
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 63d9a2471b6..813db12e83f 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -31,6 +31,9 @@ import * as urlUtils from '~/lib/utils/url_utility';
import * as commonUtils from '~/lib/utils/common_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { stubPerformanceWebAPI } from 'helpers/performance';
+import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
+import waitForPromises from 'helpers/wait_for_promises';
+import { diffMetadata } from 'jest/diffs/mock_data/diff_metadata';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -38,6 +41,8 @@ const mergeRequestDiff = { version_index: 1 };
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
const COMMIT_URL = `${TEST_HOST}/COMMIT/OLD`;
const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
+const ENDPOINT_BATCH_URL = `${TEST_HOST}/diff/endpointBatch`;
+const ENDPOINT_METADATA_URL = `${TEST_HOST}/diff/endpointMetadata`;
Vue.use(Vuex);
Vue.use(VueApollo);
@@ -77,8 +82,8 @@ describe('diffs/components/app', () => {
store.dispatch('diffs/setBaseConfig', {
endpoint: TEST_ENDPOINT,
- endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
- endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
+ endpointMetadata: ENDPOINT_METADATA_URL,
+ endpointBatch: ENDPOINT_BATCH_URL,
endpointDiffForPath: TEST_ENDPOINT,
projectPath: 'namespace/project',
dismissEndpoint: '',
@@ -126,7 +131,7 @@ describe('diffs/components/app', () => {
const fetchResolver = () => {
store.state.diffs.retrievingBatches = false;
store.state.notes.doneFetchingBatchDiscussions = true;
- store.state.notes.discussions = 'test';
+ store.state.notes.discussions = [];
return Promise.resolve({ real_size: 100 });
};
jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
@@ -861,4 +866,32 @@ describe('diffs/components/app', () => {
expect(loadSpy).not.toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
});
});
+
+ describe('pinned file', () => {
+ const pinnedFileUrl = 'http://localhost.test/pinned-file';
+ let pinnedFile;
+
+ beforeEach(() => {
+ pinnedFile = getDiffFileMock();
+ mock.onGet(pinnedFileUrl).reply(HTTP_STATUS_OK, { diff_files: [pinnedFile] });
+ mock
+ .onGet(new RegExp(ENDPOINT_BATCH_URL))
+ .reply(HTTP_STATUS_OK, { diff_files: [], pagination: {} });
+ mock.onGet(new RegExp(ENDPOINT_METADATA_URL)).reply(HTTP_STATUS_OK, diffMetadata);
+
+ createComponent({ shouldShow: true, pinnedFileUrl });
+ });
+
+ it('fetches and displays pinned file', async () => {
+ await waitForPromises();
+
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')[0].file_hash).toBe(
+ pinnedFile.file_hash,
+ );
+ });
+
+ it('shows a spinner during loading', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index d6539a5bffa..c02875963fd 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
@@ -20,6 +20,7 @@ import { truncateSha } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { TEST_HOST } from 'spec/test_constants';
import testAction from '../../__helpers__/vuex_action_helper';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
@@ -73,6 +74,7 @@ describe('DiffFileHeader component', () => {
setFileCollapsedByUser: jest.fn(),
setFileForcedOpen: jest.fn(),
reviewFile: jest.fn(),
+ unpinFile: jest.fn(),
},
},
},
@@ -87,7 +89,7 @@ describe('DiffFileHeader component', () => {
});
const findHeader = () => wrapper.findComponent({ ref: 'header' });
- const findTitleLink = () => wrapper.findComponent({ ref: 'titleWrapper' });
+ const findTitleLink = () => wrapper.findByTestId('file-title');
const findExpandButton = () => wrapper.findComponent({ ref: 'expandDiffToFullFileButton' });
const findFileActions = () => wrapper.find('.file-actions');
const findModeChangedLine = () => wrapper.findComponent({ ref: 'fileMode' });
@@ -105,7 +107,7 @@ describe('DiffFileHeader component', () => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
const store = new Vuex.Store({ ...mockStoreConfig, ...options.store });
- wrapper = shallowMount(DiffFileHeader, {
+ wrapper = shallowMountExtended(DiffFileHeader, {
propsData: {
diffFile,
canCurrentUserFork: false,
@@ -711,4 +713,23 @@ describe('DiffFileHeader component', () => {
expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(true);
});
+
+ describe('pinned file', () => {
+ beforeEach(() => {
+ window.gon.features = { pinnedFile: true };
+ });
+
+ it('has pinned URL search param', () => {
+ createComponent();
+ const url = new URL(TEST_HOST + findTitleLink().attributes('href'));
+ expect(url.searchParams.get('pin')).toBe(diffFile.file_hash);
+ });
+
+ it('can unpin file', () => {
+ createComponent({ props: { addMergeRequestButtons: true, pinned: true } });
+ const unpinButton = wrapper.findComponentByTestId('unpin-button');
+ unpinButton.vm.$emit('click');
+ expect(mockStoreConfig.modules.diffs.actions.unpinFile).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index a9fbf4632ac..444f4102e26 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -29,6 +29,7 @@ import createNotesStore from '~/notes/stores/modules';
import diffsModule from '~/diffs/store/modules';
import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { SET_PINNED_FILE_HASH } from '~/diffs/store/mutation_types';
import { getDiffFileMock } from '../mock_data/diff_file';
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -90,49 +91,6 @@ function markFileToBeRendered(store, index = 0) {
});
}
-function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
- const diffs = diffsModule();
- diffs.actions = {
- ...diffs.actions,
- prefetchFileNeighbors: prefetchFileNeighborsMock,
- saveDiffDiscussion: saveDiffDiscussionMock,
- };
-
- diffs.getters = {
- ...diffs.getters,
- diffCompareDropdownTargetVersions: () => [],
- diffCompareDropdownSourceVersions: () => [],
- };
-
- const store = new Vuex.Store({
- ...createNotesStore(),
- modules: { diffs },
- });
-
- store.state.diffs = {
- mergeRequestDiff: diffsMockData[0],
- diffFiles: [file],
- };
-
- const wrapper = shallowMountExtended(DiffFileComponent, {
- store,
- propsData: {
- file,
- canCurrentUserFork: false,
- viewDiffsFileByFile: false,
- isFirstFile: first,
- isLastFile: last,
- ...props,
- },
- ...options,
- });
-
- return {
- wrapper,
- store,
- };
-}
-
const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent);
const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area');
const findLoader = (wrapper) => wrapper.findByTestId('loader-icon');
@@ -159,15 +117,58 @@ const triggerSaveDraftNote = (wrapper, note, parent, error) =>
findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error);
describe('DiffFile', () => {
- let readableFile;
let wrapper;
let store;
let axiosMock;
+ function createComponent({
+ file = getReadableFile(),
+ first = false,
+ last = false,
+ options = {},
+ props = {},
+ } = {}) {
+ const diffs = diffsModule();
+ diffs.actions = {
+ ...diffs.actions,
+ prefetchFileNeighbors: prefetchFileNeighborsMock,
+ saveDiffDiscussion: saveDiffDiscussionMock,
+ };
+
+ diffs.getters = {
+ ...diffs.getters,
+ diffCompareDropdownTargetVersions: () => [],
+ diffCompareDropdownSourceVersions: () => [],
+ };
+
+ store = new Vuex.Store({
+ ...createNotesStore(),
+ modules: { diffs },
+ });
+
+ store.state.diffs = {
+ ...store.state.diffs,
+ mergeRequestDiff: diffsMockData[0],
+ diffFiles: [file],
+ };
+
+ wrapper = shallowMountExtended(DiffFileComponent, {
+ store,
+ propsData: {
+ file,
+ canCurrentUserFork: false,
+ viewDiffsFileByFile: false,
+ isFirstFile: first,
+ isLastFile: last,
+ ...props,
+ },
+ ...options,
+ });
+ }
+
beforeEach(() => {
- readableFile = getReadableFile();
axiosMock = new MockAdapter(axios);
- ({ wrapper, store } = createComponent({ file: readableFile }));
+ createComponent();
});
afterEach(() => {
@@ -186,7 +187,6 @@ describe('DiffFile', () => {
`('$description', ({ fileByFile }) => {
createComponent({
props: { viewDiffsFileByFile: fileByFile },
- file: readableFile,
});
if (fileByFile) {
@@ -217,11 +217,11 @@ describe('DiffFile', () => {
forceHasDiff({ store, ...file });
}
- ({ wrapper, store } = createComponent({
+ createComponent({
file: store.state.diffs.diffFiles[0],
first,
last,
- }));
+ });
await nextTick();
@@ -233,14 +233,13 @@ describe('DiffFile', () => {
);
it('emits the "first file shown" and "files end" events when in File-by-File mode', async () => {
- ({ wrapper, store } = createComponent({
- file: getReadableFile(),
+ createComponent({
first: false,
last: false,
props: {
viewDiffsFileByFile: true,
},
- }));
+ });
await nextTick();
@@ -253,11 +252,11 @@ describe('DiffFile', () => {
describe('after loading the diff', () => {
it('indicates that it loaded the file', async () => {
forceHasDiff({ store, inlineLines: [], parallelLines: [], readableText: true });
- ({ wrapper, store } = createComponent({
+ createComponent({
file: store.state.diffs.diffFiles[0],
first: true,
last: true,
- }));
+ });
jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue(getReadableFile());
jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
@@ -314,11 +313,11 @@ describe('DiffFile', () => {
`('should be $bool when { userIsLoggedIn: $loggedIn }', ({ loggedIn, bool }) => {
setLoggedIn(loggedIn);
- ({ wrapper } = createComponent({
+ createComponent({
props: {
file: store.state.diffs.diffFiles[0],
},
- }));
+ });
expect(wrapper.vm.showLocalFileReviews).toBe(bool);
});
@@ -556,7 +555,7 @@ describe('DiffFile', () => {
describe('general (other) collapsed', () => {
it('should be expandable for unreadable files', async () => {
- ({ wrapper, store } = createComponent({ file: getUnreadableFile() }));
+ createComponent({ file: getUnreadableFile() });
makeFileAutomaticallyCollapsed(store);
await nextTick();
@@ -622,7 +621,7 @@ describe('DiffFile', () => {
renderIt: true,
};
- ({ wrapper, store } = createComponent({ file }));
+ createComponent({ file });
expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false);
});
@@ -634,7 +633,7 @@ describe('DiffFile', () => {
renderIt: true,
};
- ({ wrapper, store } = createComponent({ file }));
+ createComponent({ file });
expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true);
});
@@ -656,9 +655,9 @@ describe('DiffFile', () => {
...extraProps,
};
- ({ wrapper, store } = createComponent({
+ createComponent({
file,
- }));
+ });
expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists);
},
@@ -676,9 +675,9 @@ describe('DiffFile', () => {
hasCommentForm,
};
- ({ wrapper, store } = createComponent({
+ createComponent({
file,
- }));
+ });
expect(findNoteForm(wrapper).exists()).toEqual(exists);
},
@@ -694,9 +693,9 @@ describe('DiffFile', () => {
discussions,
};
- ({ wrapper, store } = createComponent({
+ createComponent({
file,
- }));
+ });
expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists);
});
@@ -712,10 +711,10 @@ describe('DiffFile', () => {
const errorCallback = jest.fn();
beforeEach(() => {
- ({ wrapper, store } = createComponent({
+ createComponent({
file,
options: { provide: { glFeatures: { commentOnFiles: true } } },
- }));
+ });
});
it('calls saveDiffDiscussionMock', () => {
@@ -771,10 +770,10 @@ describe('DiffFile', () => {
const errorCallback = jest.fn();
beforeEach(async () => {
- ({ wrapper, store } = createComponent({
+ createComponent({
file,
options: { provide: { glFeatures: { commentOnFiles: true } } },
- }));
+ });
triggerSaveDraftNote(wrapper, note, parentElement, errorCallback);
@@ -791,4 +790,13 @@ describe('DiffFile', () => {
});
});
});
+
+ describe('pinned file', () => {
+ it('passes down pinned prop', async () => {
+ createComponent();
+ store.commit(`diffs/${SET_PINNED_FILE_HASH}`, getReadableFile().file_hash);
+ await nextTick();
+ expect(wrapper.findComponent(DiffFileHeaderComponent).props('pinned')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 6e9eb433924..bd9592e4f5e 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -6,6 +6,7 @@ import {
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
} from '~/diffs/constants';
+import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
const LINE_CODE = 'abc123';
@@ -108,15 +109,47 @@ describe('diff_row_utils', () => {
describe('lineHref', () => {
it(`should return #${LINE_CODE}`, () => {
- expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
+ expect(utils.lineHref({ line_code: LINE_CODE }, {})).toEqual(`#${LINE_CODE}`);
});
it(`should return '#' if line is undefined`, () => {
- expect(utils.lineHref()).toEqual('#');
+ expect(utils.lineHref()).toEqual('');
});
it(`should return '#' if line_code is undefined`, () => {
- expect(utils.lineHref({})).toEqual('#');
+ expect(utils.lineHref({}, {})).toEqual('');
+ });
+
+ describe('pinned file', () => {
+ beforeEach(() => {
+ window.gon.features = { pinnedFile: true };
+ });
+
+ afterEach(() => {
+ delete window.gon.features;
+ });
+
+ it(`should return pinned file URL`, () => {
+ const diffFile = getDiffFileMock();
+ expect(utils.lineHref({ line_code: LINE_CODE }, { diffFile })).toEqual(
+ `?pin=${diffFile.file_hash}#${LINE_CODE}`,
+ );
+ });
+ });
+ });
+
+ describe('pinnedFileHref', () => {
+ beforeEach(() => {
+ window.gon.features = { pinnedFile: true };
+ });
+
+ afterEach(() => {
+ delete window.gon.features;
+ });
+
+ it(`should return pinned file URL`, () => {
+ const diffFile = getDiffFileMock();
+ expect(utils.pinnedFileHref(diffFile)).toEqual(`?pin=${diffFile.file_hash}`);
});
});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index a54cf9b8bff..230839f0ecf 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -7,6 +7,9 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments';
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { SET_PINNED_FILE_HASH, SET_TREE_DATA, SET_DIFF_FILES } from '~/diffs/store/mutation_types';
+import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
+import { sortTree } from '~/ide/stores/utils';
describe('Diffs tree list component', () => {
let wrapper;
@@ -58,6 +61,14 @@ describe('Diffs tree list component', () => {
const setupFilesInState = () => {
const treeEntries = {
+ app: {
+ key: 'app',
+ path: 'app',
+ name: 'app',
+ type: 'tree',
+ tree: [],
+ opened: true,
+ },
'index.js': {
addedLines: 0,
changed: true,
@@ -71,6 +82,8 @@ describe('Diffs tree list component', () => {
type: 'blob',
parentPath: 'app',
tree: [],
+ file_path: 'app/index.js',
+ file_hash: 'app-index',
},
'test.rb': {
addedLines: 0,
@@ -85,20 +98,39 @@ describe('Diffs tree list component', () => {
type: 'blob',
parentPath: 'app',
tree: [],
+ file_path: 'app/test.rb',
+ file_hash: 'app-test',
},
- app: {
- key: 'app',
- path: 'app',
- name: 'app',
- type: 'tree',
+ LICENSE: {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'LICENSE',
+ key: 'LICENSE',
+ name: 'LICENSE',
+ path: 'LICENSE',
+ removedLines: 0,
+ tempFile: true,
+ type: 'blob',
+ parentPath: '/',
tree: [],
+ file_path: 'LICENSE',
+ file_hash: 'LICENSE',
},
};
Object.assign(store.state.diffs, {
treeEntries,
- tree: [treeEntries['index.js'], treeEntries.app],
+ tree: [
+ treeEntries.LICENSE,
+ {
+ ...treeEntries.app,
+ tree: [treeEntries['index.js'], treeEntries['test.rb']],
+ },
+ ],
});
+
+ return treeEntries;
};
describe('default', () => {
@@ -149,7 +181,7 @@ describe('Diffs tree list component', () => {
});
it('renders tree', () => {
- expect(getScroller().props('items')).toHaveLength(2);
+ expect(getScroller().props('items')).toHaveLength(4);
});
it('hides file stats', () => {
@@ -169,7 +201,7 @@ describe('Diffs tree list component', () => {
store.state.diffs.renderTreeList = false;
await nextTick();
- expect(getScroller().props('items')).toHaveLength(3);
+ expect(getScroller().props('items')).toHaveLength(5);
});
});
@@ -188,4 +220,59 @@ describe('Diffs tree list component', () => {
expect(getFileRow().props('viewedFiles')).toBe(viewedDiffFileIds);
});
});
+
+ describe('pinned file', () => {
+ const filePaths = [
+ ['nested-1.rb', 'folder/sub-folder/'],
+ ['nested-2.rb', 'folder/sub-folder/'],
+ ['nested-3.rb', 'folder/sub-folder/'],
+ ['1.rb', 'folder/'],
+ ['2.rb', 'folder/'],
+ ['3.rb', 'folder/'],
+ ['single.rb', 'folder-single/'],
+ ['root-first.rb'],
+ ['root-last.rb'],
+ ];
+
+ const pinFile = (fileHash) => {
+ store.commit(`diffs/${SET_PINNED_FILE_HASH}`, fileHash);
+ };
+
+ const setupFiles = (diffFiles) => {
+ const { treeEntries, tree } = generateTreeList(diffFiles);
+ store.commit(`diffs/${SET_DIFF_FILES}`, diffFiles);
+ store.commit(`diffs/${SET_TREE_DATA}`, {
+ treeEntries,
+ tree: sortTree(tree),
+ });
+ };
+
+ const createFile = (name, path = '') => ({
+ file_hash: name,
+ path: `${path}${name}`,
+ new_path: `${path}${name}`,
+ file_path: `${path}${name}`,
+ });
+
+ beforeEach(() => {
+ createComponent();
+ setupFiles(filePaths.map(([name, path]) => createFile(name, path)));
+ });
+
+ describe('files in folders', () => {
+ it.each(filePaths.map((path) => path[0]))('pins %s file', async (pinnedFile) => {
+ pinFile(pinnedFile);
+ await nextTick();
+ const items = getScroller().props('items');
+ expect(
+ items.map(
+ (item) =>
+ `${'─'.repeat(item.level * 2)}${item.type === 'tree' ? '📁' : ''}${
+ item.name || item.path
+ }`,
+ ),
+ ).toMatchSnapshot();
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index be3b30e8e7a..c68f3c5991e 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -11,7 +11,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
EVT_MR_PREPARED,
} from '~/diffs/constants';
-import { LOAD_SINGLE_DIFF_FAILED, BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n';
+import { BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n';
import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
@@ -28,6 +28,8 @@ import {
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import diffsEventHub from '~/diffs/event_hub';
+import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/alert');
@@ -37,6 +39,8 @@ jest.mock('~/lib/utils/secret_detection', () => ({
containsSensitiveToken: jest.requireActual('~/lib/utils/secret_detection').containsSensitiveToken,
}));
+const endpointDiffForPath = '/diffs/set/endpoint/path';
+
describe('DiffsStoreActions', () => {
let mock;
@@ -78,7 +82,6 @@ describe('DiffsStoreActions', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
- const endpointDiffForPath = '/diffs/set/endpoint/path';
const endpointCoverage = '/diffs/set/coverage_reports';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
@@ -181,7 +184,6 @@ describe('DiffsStoreActions', () => {
w: '1',
view: 'inline',
};
- const endpointDiffForPath = '/diffs/set/endpoint/path';
const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath);
const treeEntry = {
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
@@ -350,7 +352,6 @@ describe('DiffsStoreActions', () => {
w: '1',
view: 'inline',
};
- const endpointDiffForPath = '/diffs/set/endpoint/path';
const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath);
const treeEntry = {
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
@@ -490,8 +491,8 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFilesBatch', () => {
it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';
- const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 7 } };
- const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 7 } };
+ const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { total_pages: 2 } };
+ const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: { total_pages: 2 } };
mock
.onGet(
mergeUrlParams(
@@ -520,7 +521,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.fetchDiffFilesBatch,
- {},
+ undefined,
{ endpointBatch, diffViewType: 'inline', diffFiles: [], perPage: 5 },
[
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
@@ -532,7 +533,6 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
{ type: types.SET_CURRENT_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
- { type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
[],
);
@@ -690,7 +690,7 @@ describe('DiffsStoreActions', () => {
describe('setHighlightedRow', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
- return testAction(diffActions.setHighlightedRow, 'ABC_123', {}, [
+ return testAction(diffActions.setHighlightedRow, { lineCode: 'ABC_123' }, {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
{ type: types.SET_CURRENT_DIFF_FILE, payload: 'ABC' },
]);
@@ -1310,14 +1310,17 @@ describe('DiffsStoreActions', () => {
diffActions.goToFile({ state, dispatch, getters, commit }, file);
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
- expect(dispatch).toHaveBeenCalledTimes(0);
+ expect(dispatch).not.toHaveBeenCalledWith('fetchFileByFile');
});
describe('when the tree entry has not been loaded', () => {
it('updates location hash', () => {
diffActions.goToFile({ state, commit, getters, dispatch }, file);
- expect(document.location.hash).toBe('#test');
+ expect(historyPushState).toHaveBeenCalledWith(new URL(`${TEST_HOST}#test`), {
+ skipScrolling: true,
+ });
+ expect(scrollToElement).toHaveBeenCalledWith('.diff-files-holder', { duration: 0 });
});
it('loads the file and then scrolls to it', async () => {
@@ -1333,21 +1336,12 @@ describe('DiffsStoreActions', () => {
expect(commonUtils.scrollToElement).toHaveBeenCalledWith('.diff-files-holder', {
duration: 0,
});
- expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
});
- it('shows an alert when there was an error fetching the file', async () => {
- dispatch = jest.fn().mockRejectedValue();
-
+ it('unpins the file', () => {
diffActions.goToFile({ state, commit, getters, dispatch }, file);
-
- // Wait for the fetchFileByFile dispatch to return, to trigger the catch
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
- });
+ expect(dispatch).toHaveBeenCalledWith('unpinFile');
});
});
});
@@ -1969,7 +1963,7 @@ describe('DiffsStoreActions', () => {
0,
{ flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
- [],
+ [{ type: 'unpinFile' }],
);
});
@@ -1979,7 +1973,7 @@ describe('DiffsStoreActions', () => {
0,
{ viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
- [{ type: 'fetchFileByFile' }],
+ [{ type: 'unpinFile' }, { type: 'fetchFileByFile' }],
);
});
});
@@ -2120,4 +2114,84 @@ describe('DiffsStoreActions', () => {
);
});
});
+
+ describe('fetchPinnedFile', () => {
+ it('fetches pinned file', async () => {
+ const pinnedFileHref = `${TEST_HOST}/pinned-file`;
+ const pinnedFile = getDiffFileMock();
+ const diffFiles = [pinnedFile];
+ const hubSpy = jest.spyOn(diffsEventHub, '$emit');
+ mock.onGet(new RegExp(pinnedFileHref)).reply(HTTP_STATUS_OK, { diff_files: diffFiles });
+
+ await testAction(
+ diffActions.fetchPinnedFile,
+ pinnedFileHref,
+ {},
+ [
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
+ { type: types.SET_RETRIEVING_BATCHES, payload: true },
+ {
+ type: types.SET_DIFF_DATA_BATCH,
+ payload: { diff_files: diffFiles, updatePosition: false },
+ },
+ { type: types.SET_PINNED_FILE_HASH, payload: pinnedFile.file_hash },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: pinnedFile.file_hash },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
+ { type: types.SET_RETRIEVING_BATCHES, payload: false },
+ ],
+ [],
+ );
+
+ jest.runAllTimers();
+ expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
+ expect(handleLocationHash).toHaveBeenCalled();
+ });
+
+ it('handles load error', async () => {
+ const pinnedFileHref = `${TEST_HOST}/pinned-file`;
+ const hubSpy = jest.spyOn(diffsEventHub, '$emit');
+ mock.onGet(new RegExp(pinnedFileHref)).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ try {
+ await testAction(
+ diffActions.fetchPinnedFile,
+ pinnedFileHref,
+ {},
+ [
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
+ { type: types.SET_RETRIEVING_BATCHES, payload: true },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
+ { type: types.SET_RETRIEVING_BATCHES, payload: false },
+ ],
+ [],
+ );
+ } catch (error) {
+ expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ jest.runAllTimers();
+ expect(hubSpy).not.toHaveBeenCalledWith('diffFilesModified');
+ expect(handleLocationHash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('unpinFile', () => {
+ it('unpins pinned file', () => {
+ const pinnedFile = getDiffFileMock();
+ setWindowLocation(`${TEST_HOST}/?pin=${pinnedFile.file_hash}#${pinnedFile.file_hash}_10_10`);
+ testAction(
+ diffActions.unpinFile,
+ undefined,
+ { pinnedFile },
+ [{ type: types.SET_PINNED_FILE_HASH, payload: null }],
+ [],
+ );
+ expect(window.location.hash).toBe('');
+ expect(window.location.search).toBe('');
+ });
+
+ it('does nothing when no pinned file present', () => {
+ testAction(diffActions.unpinFile, undefined, {}, [], []);
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 8097f0976f6..cb0f40534fe 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -1,6 +1,7 @@
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
import * as getters from '~/diffs/store/getters';
import state from '~/diffs/store/modules/diff_state';
+import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
import discussion from '../mock_data/diff_discussions';
describe('Diffs Module Getters', () => {
@@ -495,4 +496,35 @@ describe('Diffs Module Getters', () => {
},
);
});
+
+ describe('diffFiles', () => {
+ it('proxies diffFiles state', () => {
+ const diffFiles = [getDiffFileMock()];
+ expect(getters.diffFiles({ diffFiles }, {})).toBe(diffFiles);
+ });
+
+ it('pins the file', () => {
+ const pinnedFile = getDiffFileMock();
+ const regularFile = getDiffFileMock();
+ const diffFiles = [regularFile, pinnedFile];
+ expect(getters.diffFiles({ diffFiles }, { pinnedFile })).toStrictEqual([
+ pinnedFile,
+ regularFile,
+ ]);
+ });
+ });
+
+ describe('pinnedFile', () => {
+ it('returns pinnedFile', () => {
+ const pinnedFile = getDiffFileMock();
+ const diffFiles = [pinnedFile];
+ expect(getters.pinnedFile({ diffFiles, pinnedFileHash: pinnedFile.file_hash }, {})).toBe(
+ pinnedFile,
+ );
+ });
+
+ it('returns null if no pinned file is set', () => {
+ expect(getters.pinnedFile({}, {})).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index a5be41aa69f..8d52cd39542 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -92,7 +92,7 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('SET_DIFF_DATA_BATCH_DATA', () => {
+ describe('SET_DIFF_DATA_BATCH', () => {
it('should set diff data batch type properly', () => {
const mockFile = getDiffFileMock();
const state = {
@@ -108,6 +108,39 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].collapsed).toEqual(false);
expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
});
+
+ it('should update diff position by default', () => {
+ const mockFile = getDiffFileMock();
+ const state = {
+ diffFiles: [mockFile, { ...mockFile, file_hash: 'foo', file_path: 'foo' }],
+ treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } },
+ };
+ const diffMock = {
+ diff_files: [mockFile],
+ };
+
+ mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
+
+ expect(state.diffFiles[1].file_hash).toBe(mockFile.file_hash);
+ expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
+ });
+
+ it('should not update diff position', () => {
+ const mockFile = getDiffFileMock();
+ const state = {
+ diffFiles: [mockFile, { ...mockFile, file_hash: 'foo', file_path: 'foo' }],
+ treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } },
+ };
+ const diffMock = {
+ diff_files: [mockFile],
+ updatePosition: false,
+ };
+
+ mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
+
+ expect(state.diffFiles[0].file_hash).toBe(mockFile.file_hash);
+ expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
+ });
});
describe('SET_COVERAGE_DATA', () => {
@@ -122,6 +155,17 @@ describe('DiffsStoreMutations', () => {
});
});
+ describe('SET_DIFF_TREE_ENTRY', () => {
+ it('should set tree entry', () => {
+ const file = getDiffFileMock();
+ const state = { treeEntries: { [file.file_path]: {} } };
+
+ mutations[types.SET_DIFF_TREE_ENTRY](state, file);
+
+ expect(state.treeEntries[file.file_path].diffLoaded).toBe(true);
+ });
+ });
+
describe('SET_DIFF_VIEW_TYPE', () => {
it('should set diff view type properly', () => {
const state = {};
@@ -1076,4 +1120,15 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].viewer.forceOpen).toBe(true);
});
});
+
+ describe('SET_PINNED_FILE_HASH', () => {
+ it('set pinned file hash', () => {
+ const state = {};
+ const file = getDiffFileMock();
+
+ mutations[types.SET_PINNED_FILE_HASH](state, file.file_hash);
+
+ expect(state.pinnedFileHash).toBe(file.file_hash);
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 6331269d6e8..019ed663d82 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -476,6 +476,17 @@ describe('DiffsStoreUtils', () => {
expect(updatedFilesList).toEqual([mock, fakeNewFile]);
});
+ it('updates diff position', () => {
+ const priorFiles = [mock, { ...mock, file_hash: 'foo', file_path: 'foo' }];
+ const updatedFilesList = utils.prepareDiffData({
+ diff: { diff_files: [mock] },
+ priorFiles,
+ updatePosition: true,
+ });
+
+ expect(updatedFilesList[1].file_hash).toEqual(mock.file_hash);
+ });
+
it('completes an existing split diff without overwriting existing diffs', () => {
// The current state has a file that has only loaded inline lines
const priorFiles = [{ ...mock }];
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index f2d11f91eb9..26c97a7cb41 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -11,6 +11,8 @@ import {
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
+ GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -115,6 +117,51 @@ describe('GroupItemComponent', () => {
wrapper.destroy();
});
});
+
+ describe('visibilityTooltip', () => {
+ describe('if item represents group', () => {
+ it.each`
+ visibilityLevel | visibilityTooltip
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]}
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]}
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]}
+ `(
+ 'should return corresponding text when visibility level is $visibilityLevel',
+ ({ visibilityLevel, visibilityTooltip }) => {
+ const group = { ...mockParentGroupItem };
+
+ group.type = 'group';
+ group.visibility = visibilityLevel;
+ wrapper = createComponent({ group });
+
+ expect(wrapper.vm.visibilityTooltip).toBe(visibilityTooltip);
+ wrapper.destroy();
+ },
+ );
+ });
+
+ describe('if item represents project', () => {
+ it.each`
+ visibilityLevel | visibilityTooltip
+ ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]}
+ ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]}
+ ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]}
+ `(
+ 'should return corresponding text when visibility level is $visibilityLevel',
+ ({ visibilityLevel, visibilityTooltip }) => {
+ const group = { ...mockParentGroupItem };
+
+ group.type = 'project';
+ group.lastActivityAt = '2017-04-09T18:40:39.101Z';
+ group.visibility = visibilityLevel;
+ wrapper = createComponent({ group });
+
+ expect(wrapper.vm.visibilityTooltip).toBe(visibilityTooltip);
+ wrapper.destroy();
+ },
+ );
+ });
+ });
});
describe('methods', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
index 26d4e4dbb4f..bdb3db7a1b9 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js
@@ -1,4 +1,4 @@
-import { GlTable } from '@gitlab/ui';
+import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -21,6 +21,7 @@ describe('Packages protection rules project settings', () => {
};
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findTable = () => wrapper.findComponent(GlTable);
+ const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTableRows = () => findTable().find('tbody').findAll('tr');
const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => {
@@ -56,25 +57,41 @@ describe('Packages protection rules project settings', () => {
expect(findTable().exists()).toBe(true);
});
- it('renders table with container registry protection rules', async () => {
- createComponent({ mountFn: mount });
+ describe('table package protection rules', () => {
+ it('renders table with packages protection rules', async () => {
+ createComponent({ mountFn: mount });
- await waitForPromises();
+ await waitForPromises();
- expect(findTable().exists()).toBe(true);
+ expect(findTable().exists()).toBe(true);
- packagesProtectionRulesData.forEach((protectionRule, i) => {
- expect(findTableRows().at(i).text()).toContain(protectionRule.packageNamePattern);
- expect(findTableRows().at(i).text()).toContain(protectionRule.packageType);
- expect(findTableRows().at(i).text()).toContain(protectionRule.pushProtectedUpToAccessLevel);
+ packagesProtectionRulesData.forEach((protectionRule, i) => {
+ expect(findTableRows().at(i).text()).toContain(protectionRule.packageNamePattern);
+ expect(findTableRows().at(i).text()).toContain(protectionRule.packageType);
+ expect(findTableRows().at(i).text()).toContain(protectionRule.pushProtectedUpToAccessLevel);
+ });
});
- });
- it('renders table with pagination', async () => {
- createComponent();
+ it('displays table in busy state and shows loading icon inside table', async () => {
+ createComponent({ mountFn: mount });
- await waitForPromises();
+ expect(findTableLoadingIcon().exists()).toBe(true);
+ expect(findTableLoadingIcon().attributes('aria-label')).toBe('Loading');
- expect(findTable().exists()).toBe(true);
+ expect(findTable().attributes('aria-busy')).toBe('true');
+
+ await waitForPromises();
+
+ expect(findTableLoadingIcon().exists()).toBe(false);
+ expect(findTable().attributes('aria-busy')).toBe('false');
+ });
+
+ it('renders table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index 4e3554131c6..75b239d2d94 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -74,20 +74,14 @@ describe('Access Level Dropdown', () => {
const createComponent = ({
accessLevelsData = mockAccessLevelsData,
accessLevel = ACCESS_LEVELS.PUSH,
- hasLicense,
- label,
- disabled,
- preselectedItems,
stubs = {},
+ ...optionalProps
} = {}) => {
wrapper = shallowMountExtended(AccessDropdown, {
propsData: {
accessLevelsData,
accessLevel,
- hasLicense,
- label,
- disabled,
- preselectedItems,
+ ...optionalProps,
},
stubs: {
GlSprintf,
@@ -114,10 +108,19 @@ describe('Access Level Dropdown', () => {
it('should make an api call for users, groups && deployKeys when user has a license', () => {
createComponent();
expect(getUsers).toHaveBeenCalled();
- expect(getGroups).toHaveBeenCalled();
+ expect(getGroups).toHaveBeenCalledWith({ withProjectAccess: false });
expect(getDeployKeys).toHaveBeenCalled();
});
+ describe('withProjectAccess', () => {
+ it('should make an api call for users, groups && deployKeys when user has a license', () => {
+ createComponent({ groupsWithProjectAccess: true });
+ expect(getUsers).toHaveBeenCalled();
+ expect(getGroups).toHaveBeenCalledWith({ withProjectAccess: true });
+ expect(getDeployKeys).toHaveBeenCalled();
+ });
+ });
+
it('should make an api call for deployKeys but not for users or groups when user does not have a license', () => {
createComponent({ hasLicense: false });
expect(getUsers).not.toHaveBeenCalled();
@@ -132,7 +135,7 @@ describe('Access Level Dropdown', () => {
findSearchBox().vm.$emit('input', query);
await nextTick();
expect(getUsers).toHaveBeenCalledWith(query);
- expect(getGroups).toHaveBeenCalled();
+ expect(getGroups).toHaveBeenCalledWith({ withProjectAccess: false });
expect(getDeployKeys).toHaveBeenCalledWith(query);
});
});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 976866af27c..d063db1e34b 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import { nextTick } from 'vue';
import { file } from 'jest/ide/helpers';
import { escapeFileUrl } from '~/lib/utils/url_utility';
@@ -153,4 +154,16 @@ describe('File row component', () => {
expect(wrapper.findComponent(FileIcon).props('submodule')).toBe(submodule);
});
+
+ it('renders pinned icon', () => {
+ createComponent({
+ file: {
+ ...file(),
+ pinned: true,
+ },
+ level: 0,
+ });
+
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('thumbtack');
+ });
});
diff --git a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
index b30501cce21..e0b090f7ff9 100644
--- a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
table_name: table_name,
partitioning_column: partitioning_column,
parent_table_name: parent_table_name,
- zero_partition_value: partitioning_default
+ zero_partition_value: zero_partition_value
)
end
@@ -24,107 +24,121 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
let(:async) { false }
- it 'adds a check constraint' do
- expect { prepare }.to change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier)
- .count
- }.from(0).to(1)
- end
-
- context 'when it fails to add constraint' do
- before do
- allow(migration_context).to receive(:add_check_constraint)
- end
-
- it 'raises UnableToPartition error' do
- expect { prepare }
- .to raise_error(described_class::UnableToPartition)
- .and change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier)
- .count
- }.by(0)
- end
- end
-
- context 'when async' do
- let(:async) { true }
-
- it 'adds a NOT VALID check constraint' do
+ shared_examples 'runs #prepare_for_partitioning' do
+ it 'adds a check constraint' do
expect { prepare }.to change {
Gitlab::Database::PostgresConstraint
.check_constraints
.by_table_identifier(table_identifier)
.count
}.from(0).to(1)
+ end
- constraint =
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier)
- .last
+ context 'when it fails to add constraint' do
+ before do
+ allow(migration_context).to receive(:add_check_constraint)
+ end
- expect(constraint.definition).to end_with('NOT VALID')
+ it 'raises UnableToPartition error' do
+ expect { prepare }
+ .to raise_error(described_class::UnableToPartition)
+ .and change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.by(0)
+ end
end
- it 'adds a PostgresAsyncConstraintValidation record' do
- expect { prepare }.to change {
- Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.count
- }.by(1)
+ context 'when async' do
+ let(:async) { true }
- record = Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation
- .where(table_name: table_name).last
+ it 'adds a NOT VALID check constraint' do
+ expect { prepare }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.from(0).to(1)
- expect(record.name).to eq described_class::PARTITIONING_CONSTRAINT_NAME
- expect(record).to be_check_constraint
- end
+ constraint =
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .last
- context 'when constraint exists but is not valid' do
- before do
- converter.prepare_for_partitioning(async: true)
+ expect(constraint.definition).to end_with('NOT VALID')
end
- it 'validates the check constraint' do
+ it 'adds a PostgresAsyncConstraintValidation record' do
expect { prepare }.to change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier).first.constraint_valid?
- }.from(false).to(true)
+ Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.count
+ }.by(1)
+
+ record = Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation
+ .where(table_name: table_name).last
+
+ expect(record.name).to eq described_class::PARTITIONING_CONSTRAINT_NAME
+ expect(record).to be_check_constraint
end
- context 'when it fails to validate constraint' do
+ context 'when constraint exists but is not valid' do
before do
- allow(migration_context).to receive(:validate_check_constraint)
+ converter.prepare_for_partitioning(async: true)
end
- it 'raises UnableToPartition error' do
- expect { prepare }
- .to raise_error(described_class::UnableToPartition,
- starting_with('Error validating partitioning constraint'))
- .and change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier)
- .count
- }.by(0)
+ it 'validates the check constraint' do
+ expect { prepare }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier).first.constraint_valid?
+ }.from(false).to(true)
end
- end
- end
- context 'when constraint exists and is valid' do
- before do
- converter.prepare_for_partitioning(async: false)
+ context 'when it fails to validate constraint' do
+ before do
+ allow(migration_context).to receive(:validate_check_constraint)
+ end
+
+ it 'raises UnableToPartition error' do
+ expect { prepare }
+ .to raise_error(described_class::UnableToPartition,
+ starting_with('Error validating partitioning constraint'))
+ .and change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.by(0)
+ end
+ end
end
- it 'raises UnableToPartition error' do
- expect(Gitlab::AppLogger).to receive(:info).with(starting_with('Nothing to do'))
- prepare
+ context 'when constraint exists and is valid' do
+ before do
+ converter.prepare_for_partitioning(async: false)
+ end
+
+ it 'raises UnableToPartition error' do
+ expect(Gitlab::AppLogger).to receive(:info).with(starting_with('Nothing to do'))
+ prepare
+ end
end
end
end
+
+ context 'when a single partitioning value is given' do
+ let(:zero_partition_value) { single_partitioning_value }
+
+ include_examples 'runs #prepare_for_partitioning'
+ end
+
+ context 'when multiple partitioning values are given' do
+ let(:zero_partition_value) { multiple_partitioning_values }
+
+ include_examples 'runs #prepare_for_partitioning'
+ end
end
describe '#revert_preparation_for_partitioning' do
@@ -132,15 +146,29 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
converter.prepare_for_partitioning
end
+ shared_examples 'runs #revert_preparation_for_partitioning' do
+ it 'removes a check constraint' do
+ expect { revert_prepare }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier("#{connection.current_schema}.#{table_name}")
+ .count
+ }.from(1).to(0)
+ end
+ end
+
subject(:revert_prepare) { converter.revert_preparation_for_partitioning }
- it 'removes a check constraint' do
- expect { revert_prepare }.to change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier("#{connection.current_schema}.#{table_name}")
- .count
- }.from(1).to(0)
+ context 'when a single partitioning value is given' do
+ let(:zero_partition_value) { single_partitioning_value }
+
+ include_examples 'runs #revert_preparation_for_partitioning'
+ end
+
+ context 'when multiple partitioning values are given' do
+ let(:zero_partition_value) { multiple_partitioning_values }
+
+ include_examples 'runs #revert_preparation_for_partitioning'
end
end
@@ -153,128 +181,146 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
converter.prepare_for_partitioning(async: async)
end
- context 'when the primary key is incorrect' do
- before do
- connection.execute(<<~SQL)
- alter table #{referencing_table_name} drop constraint fk_referencing; -- this depends on the primary key
- alter table #{other_referencing_table_name} drop constraint fk_referencing_other; -- this does too
- alter table #{table_name} drop constraint #{table_name}_pkey;
- alter table #{table_name} add constraint #{table_name}_pkey PRIMARY KEY (id);
- SQL
- end
+ shared_examples 'runs partition method' do
+ context 'when the primary key is incorrect' do
+ before do
+ connection.execute(<<~SQL)
+ alter table #{referencing_table_name} drop constraint fk_referencing; -- this depends on the primary key
+ alter table #{other_referencing_table_name} drop constraint fk_referencing_other; -- this does too
+ alter table #{table_name} drop constraint #{table_name}_pkey;
+ alter table #{table_name} add constraint #{table_name}_pkey PRIMARY KEY (id);
+ SQL
+ end
- it 'throws a reasonable error message' do
- expect { partition }.to raise_error(described_class::UnableToPartition, /#{partitioning_column}/)
+ it 'throws a reasonable error message' do
+ expect { partition }.to raise_error(described_class::UnableToPartition, /#{partitioning_column}/)
+ end
end
- end
- context 'when there is not a supporting check constraint' do
- before do
- connection.execute(<<~SQL)
- alter table #{table_name} drop constraint partitioning_constraint;
- SQL
- end
+ context 'when there is not a supporting check constraint' do
+ before do
+ connection.execute(<<~SQL)
+ alter table #{table_name} drop constraint partitioning_constraint;
+ SQL
+ end
- it 'throws a reasonable error message' do
- expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./)
+ it 'throws a reasonable error message' do
+ expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./)
+ end
end
- end
- context 'when supporting check constraint is not valid' do
- let(:async) { true }
+ context 'when supporting check constraint is not valid' do
+ let(:async) { true }
- it 'throws a reasonable error message' do
- expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./)
+ it 'throws a reasonable error message' do
+ expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./)
+ end
end
- end
-
- it 'migrates the table to a partitioned table' do
- fks_before = migration_context.foreign_keys(table_name)
-
- partition
- expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
- expect(migration_context.foreign_keys(parent_table_name).map(&:options)).to match_array(fks_before.map(&:options))
+ it 'migrates the table to a partitioned table' do
+ fks_before = migration_context.foreign_keys(table_name)
- connection.execute(<<~SQL)
- insert into #{table_name} (referenced_id, other_referenced_id) select #{referenced_table_name}.id, #{other_referenced_table_name}.id from #{referenced_table_name}, #{other_referenced_table_name};
- SQL
+ partition
- # Create a second partition
- connection.execute(<<~SQL)
- create table #{table_name}2 partition of #{parent_table_name} FOR VALUES IN (2)
- SQL
+ expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
+ expect(migration_context.foreign_keys(parent_table_name)
+ .map(&:options)).to match_array(fks_before.map(&:options))
- parent_model.create!(partitioning_column => 2, :referenced_id => 1, :other_referenced_id => 1)
- expect(parent_model.pluck(:id)).to match_array([1, 2, 3])
-
- expect { referencing_model.create!(partitioning_column => 1, :ref_id => 1) }.not_to raise_error
- end
+ connection.execute(<<~SQL)
+ insert into #{table_name} (referenced_id, other_referenced_id) select #{referenced_table_name}.id, #{other_referenced_table_name}.id from #{referenced_table_name}, #{other_referenced_table_name};
+ SQL
- context 'when the existing table is owned by a different user' do
- before do
+ # Create a second partition
connection.execute(<<~SQL)
- CREATE USER other_user SUPERUSER;
- ALTER TABLE #{table_name} OWNER TO other_user;
+ create table #{table_name}2 partition of #{parent_table_name} FOR VALUES IN (2)
SQL
- end
- let(:current_user) { model.connection.select_value('select current_user') }
+ parent_model.create!(partitioning_column => 2, :referenced_id => 1, :other_referenced_id => 1)
+ expect(parent_model.pluck(:id)).to match_array([1, 2, 3])
- it 'partitions without error' do
- expect { partition }.not_to raise_error
+ expect { referencing_model.create!(partitioning_column => 1, :ref_id => 1) }.not_to raise_error
end
- end
- context 'when an error occurs during the conversion' do
- before do
- # Set up the fault that we'd like to inject
- fault.call
- end
+ context 'when the existing table is owned by a different user' do
+ before do
+ connection.execute(<<~SQL)
+ CREATE USER other_user SUPERUSER;
+ ALTER TABLE #{table_name} OWNER TO other_user;
+ SQL
+ end
- let(:old_fks) do
- Gitlab::Database::PostgresForeignKey.by_referenced_table_identifier(table_identifier).not_inherited
- end
+ let(:current_user) { model.connection.select_value('select current_user') }
- let(:new_fks) do
- Gitlab::Database::PostgresForeignKey.by_referenced_table_identifier(parent_table_identifier).not_inherited
+ it 'partitions without error' do
+ expect { partition }.not_to raise_error
+ end
end
- context 'when partitioning fails the first time' do
- where(:case_name, :fault) do
- [
- ["creating parent table", lazy { fail_sql_matching(/CREATE/i) }],
- ["adding the first foreign key", lazy { fail_adding_fk(parent_table_name, referenced_table_name) }],
- ["adding the second foreign key", lazy { fail_adding_fk(parent_table_name, other_referenced_table_name) }],
- ["attaching table", lazy { fail_sql_matching(/ATTACH/i) }]
- ]
+ context 'when an error occurs during the conversion' do
+ before do
+ # Set up the fault that we'd like to inject
+ fault.call
end
- with_them do
- it 'recovers from a fault', :aggregate_failures do
- expect { converter.partition }.to raise_error(/fault/)
+ let(:old_fks) do
+ Gitlab::Database::PostgresForeignKey.by_referenced_table_identifier(table_identifier).not_inherited
+ end
- expect { converter.partition }.not_to raise_error
- expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
+ let(:new_fks) do
+ Gitlab::Database::PostgresForeignKey.by_referenced_table_identifier(parent_table_identifier).not_inherited
+ end
+
+ context 'when partitioning fails the first time' do
+ where(:case_name, :fault) do
+ [
+ ["creating parent table", lazy { fail_sql_matching(/CREATE/i) }],
+ ["adding the first foreign key", lazy { fail_adding_fk(parent_table_name, referenced_table_name) }],
+ ["adding the second foreign key", lazy do
+ fail_adding_fk(parent_table_name, other_referenced_table_name)
+ end],
+ ["attaching table", lazy { fail_sql_matching(/ATTACH/i) }]
+ ]
+ end
+
+ with_them do
+ it 'recovers from a fault', :aggregate_failures do
+ expect { converter.partition }.to raise_error(/fault/)
+
+ expect { converter.partition }.not_to raise_error
+ expect(Gitlab::Database::PostgresPartition.for_parent_table(parent_table_name).count).to eq(1)
+ end
end
end
end
- end
- context 'when table has LFK triggers' do
- before do
- migration_context.track_record_deletions(table_name)
- end
+ context 'when table has LFK triggers' do
+ before do
+ migration_context.track_record_deletions(table_name)
+ end
- it 'moves the trigger on the parent table', :aggregate_failures do
- expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
+ it 'moves the trigger on the parent table', :aggregate_failures do
+ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
- expect { partition }.not_to raise_error
+ expect { partition }.not_to raise_error
- expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
- expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy
+ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
+ expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy
+ end
end
end
+
+ context 'when a single partitioning value is given' do
+ let(:zero_partition_value) { single_partitioning_value }
+
+ include_examples 'runs partition method'
+ end
+
+ context 'when multiple partitioning values are given' do
+ # Because of the common spec on line 220
+ let(:zero_partition_value) { [1, 3, 4] }
+
+ include_examples 'runs partition method'
+ end
end
describe '#revert_partitioning' do
@@ -285,49 +331,67 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ
subject(:revert_conversion) { converter.revert_partitioning }
- it 'detaches the partition' do
- expect { revert_conversion }.to change {
- Gitlab::Database::PostgresPartition
- .for_parent_table(parent_table_name).count
- }.from(1).to(0)
- end
-
- it 'does not drop the child partition' do
- expect { revert_conversion }.not_to change { table_oid(table_name) }
- end
+ shared_examples 'runs #revert_partitioning' do
+ it 'detaches the partition' do
+ expect { revert_conversion }.to change {
+ Gitlab::Database::PostgresPartition
+ .for_parent_table(parent_table_name).count
+ }.from(1).to(0)
+ end
- it 'removes the parent table' do
- expect { revert_conversion }.to change { table_oid(parent_table_name).present? }.from(true).to(false)
- end
+ it 'does not drop the child partition' do
+ expect { revert_conversion }.not_to change { table_oid(table_name) }
+ end
- it 're-adds the check constraint' do
- expect { revert_conversion }.to change {
- Gitlab::Database::PostgresConstraint
- .check_constraints
- .by_table_identifier(table_identifier)
- .count
- }.by(1)
- end
+ it 'removes the parent table' do
+ expect { revert_conversion }.to change { table_oid(parent_table_name).present? }.from(true).to(false)
+ end
- it 'moves sequences back to the original table' do
- expect { revert_conversion }.to change { converter.send(:sequences_owned_by, table_name).count }.from(0)
- .and change { converter.send(:sequences_owned_by, parent_table_name).count }.to(0)
- end
+ it 're-adds the check constraint' do
+ expect { revert_conversion }.to change {
+ Gitlab::Database::PostgresConstraint
+ .check_constraints
+ .by_table_identifier(table_identifier)
+ .count
+ }.by(1)
+ end
- context 'when table has LFK triggers' do
- before do
- migration_context.track_record_deletions(parent_table_name)
- migration_context.track_record_deletions(table_name)
+ it 'moves sequences back to the original table' do
+ expect { revert_conversion }.to change { converter.send(:sequences_owned_by, table_name).count }
+ .from(0)
+ .and change {
+ converter.send(
+ :sequences_owned_by, parent_table_name).count
+ }.to(0)
end
- it 'restores the trigger on the partition', :aggregate_failures do
- expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
- expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy
+ context 'when table has LFK triggers' do
+ before do
+ migration_context.track_record_deletions(parent_table_name)
+ migration_context.track_record_deletions(table_name)
+ end
+
+ it 'restores the trigger on the partition', :aggregate_failures do
+ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
+ expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy
- expect { revert_conversion }.not_to raise_error
+ expect { revert_conversion }.not_to raise_error
- expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
+ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy
+ end
end
end
+
+ context 'when a single partitioning value is given' do
+ let(:zero_partition_value) { single_partitioning_value }
+
+ include_examples 'runs #revert_partitioning'
+ end
+
+ context 'when multiple partitioning values are given' do
+ let(:zero_partition_value) { multiple_partitioning_values }
+
+ include_examples 'runs #revert_partitioning'
+ end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/namespace_packages_spec.rb
index 949acdb17e1..d655085a30f 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/namespace_packages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package_registry do
+RSpec.describe API::Terraform::Modules::V1::NamespacePackages, feature_category: :package_registry do
include_context 'for terraform modules api setup'
using RSpec::Parameterized::TableSyntax
@@ -10,17 +10,19 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") }
let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
- subject { get(url, headers: headers) }
+ subject(:get_versions) { get(url, headers: headers) }
context 'with a conflicting package name' do
- let!(:conflicting_package) { create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0') }
+ let!(:conflicting_package) do
+ create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0')
+ end
before do
group.add_developer(user)
end
it 'returns only one version' do
- subject
+ get_versions
expect(json_response['modules'][0]['versions'].size).to eq(1)
expect(json_response['modules'][0]['versions'][0]['version']).to eq('1.0.0')
@@ -77,14 +79,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/download' do
- context 'empty registry' do
+ context 'with empty registry' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/module-2/system/download") }
let(:headers) { {} }
- subject { get(url, headers: headers) }
+ subject(:get_download) { get(url, headers: headers) }
it 'returns not found when there is no module' do
- subject
+ get_download
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -150,14 +152,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system' do
- context 'empty registry' do
+ context 'with empty registry' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/non-existent/system") }
let(:headers) { { 'Authorization' => "Bearer #{tokens[:personal_access_token]}" } }
- subject { get(url, headers: headers) }
+ subject(:get_module) { get(url, headers: headers) }
it 'returns not found when there is no module' do
- subject
+ get_module
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -221,16 +223,16 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}") }
let(:headers) { {} }
- subject { get(url, headers: headers) }
+ subject(:get_module_version) { get(url, headers: headers) }
- context 'not found' do
+ context 'when not found' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/2.0.0") }
let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
subject { get(url, headers: headers) }
it 'returns not found when the specified version is not present in the registry' do
- subject
+ get_module_version
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -343,7 +345,10 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
end
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file' do
- let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
+ let(:url) do
+ api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}")
+ end
+
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
@@ -352,7 +357,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
}
end
- subject { get(url, headers: headers) }
+ subject(:get_file) { get(url, headers: headers) }
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
@@ -414,8 +419,15 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
end
context 'with package file pending destruction' do
- let_it_be(:package) { create(:package, package_type: :terraform_module, project: project, name: "module-555/pending-destruction", version: '1.0.0') }
- let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, :xml, package: package) }
+ let_it_be(:package) do
+ create(:package, package_type: :terraform_module, project: project, name: "module-555/pending-destruction",
+ version: '1.0.0')
+ end
+
+ let_it_be(:package_file_pending_destruction) do
+ create(:package_file, :pending_destruction, :xml, package: package)
+ end
+
let_it_be(:package_file) { create(:package_file, :terraform_module, package: package) }
let(:token) { tokens[:personal_access_token] }
@@ -426,7 +438,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
end
it 'does not return them' do
- subject
+ get_file
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to eq(package_file_pending_destruction.file.file.read)
diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
index 70c4eb49698..574bc05827a 100644
--- a/spec/services/ci/create_pipeline_service/partitioning_spec.rb
+++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
let(:pipeline) { service.execute(:push).payload }
- let(:current_partition_id) { ci_testing_partition_id }
+ let(:current_partition_id) { ci_testing_partition_id_for_check_constraints }
before do
stub_ci_pipeline_yaml_file(config)
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index fb2292bfe64..4ea7bb89d61 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Issuable::CommonSystemNotesService, feature_category: :team_plann
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
it_behaves_like 'system note creation', { discussion_locked: true }, 'locked the discussion in this issue'
- it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'added time estimate of 5s'
context 'when new label is added' do
let(:label) { create(:label, project: project) }
@@ -144,7 +144,7 @@ RSpec.describe Issuable::CommonSystemNotesService, feature_category: :team_plann
end
context 'when setting an estimae' do
- it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate', false
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'added time estimate of 5s', false
end
end
end
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index cf994220946..6502aa5d2a2 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -198,11 +198,13 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
let(:action) { 'time_tracking' }
end
- context 'with a time estimate' do
- it 'sets the note text' do
+ context 'when adding a time estimate' do
+ before do
noteable.update_attribute(:time_estimate, 277200)
+ end
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ it 'sets the note text' do
+ expect(subject.note).to eq "added time estimate of 1w 4d 5h"
end
context 'when time_tracking_limit_to_hours setting is true' do
@@ -211,16 +213,32 @@ RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_plann
end
it 'sets the note text' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to eq "changed time estimate to 77h"
+ expect(subject.note).to eq "added time estimate of 77h"
end
end
end
- context 'without a time estimate' do
+ context 'when removing a time estimate' do
+ before do
+ noteable.update_attribute(:time_estimate, 277200)
+ noteable.save!
+ noteable.update_attribute(:time_estimate, 0)
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed time estimate of 1w 4d 5h"
+ end
+ end
+
+ context 'when changing a time estimate' do
+ before do
+ noteable.update_attribute(:time_estimate, 277200)
+ noteable.save!
+ noteable.update_attribute(:time_estimate, 3600)
+ end
+
it 'sets the note text' do
- expect(subject.note).to eq "removed time estimate"
+ expect(subject.note).to eq "changed time estimate to 1h from 1w 4d 5h"
end
end
diff --git a/spec/support/helpers/models/ci/partitioning_testing/partition_identifiers.rb b/spec/support/helpers/models/ci/partitioning_testing/partition_identifiers.rb
index aa091095fb6..e139f0c9fb3 100644
--- a/spec/support/helpers/models/ci/partitioning_testing/partition_identifiers.rb
+++ b/spec/support/helpers/models/ci/partitioning_testing/partition_identifiers.rb
@@ -8,6 +8,10 @@ module Ci
def ci_testing_partition_id
99999
end
+
+ def ci_testing_partition_id_for_check_constraints
+ 101
+ end
end
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index b93442a87a9..6aac031be6f 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -8115,7 +8115,6 @@
- './spec/requests/api/tags_spec.rb'
- './spec/requests/api/task_completion_status_spec.rb'
- './spec/requests/api/templates_spec.rb'
-- './spec/requests/api/terraform/modules/v1/packages_spec.rb'
- './spec/requests/api/terraform/state_spec.rb'
- './spec/requests/api/terraform/state_version_spec.rb'
- './spec/requests/api/todos_spec.rb'
diff --git a/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb
index 3d978a6fde4..fec11349b62 100644
--- a/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/database/partitioning/list_partitioning_shared_context.rb
@@ -13,6 +13,8 @@ RSpec.shared_context 'with a table structure for converting a table to a list pa
let(:table_identifier) { "#{connection.current_schema}.#{table_name}" }
let(:partitioning_column) { :partition_number }
let(:partitioning_default) { 1 }
+ let(:single_partitioning_value) { 1 }
+ let(:multiple_partitioning_values) { [1, 2, 3, 4] }
let(:referenced_table_name) { '_test_referenced_table' }
let(:other_referenced_table_name) { '_test_other_referenced_table' }
let(:referencing_table_name) { '_test_referencing_table' }
diff --git a/spec/views/devise/shared/_footer.html.haml_spec.rb b/spec/views/devise/shared/_footer.html.haml_spec.rb
new file mode 100644
index 00000000000..58fe9c1073c
--- /dev/null
+++ b/spec/views/devise/shared/_footer.html.haml_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'devise/shared/_footer', feature_category: :system_access do
+ subject { render && rendered }
+
+ context 'when public visibility is restricted' do
+ before do
+ allow(view).to receive(:public_visibility_restricted?).and_return(true)
+ end
+
+ it { is_expected.not_to have_link(_('Explore'), href: explore_root_path) }
+ it { is_expected.not_to have_link(_('Help'), href: help_path) }
+ end
+
+ context 'when public visibility is not restricted' do
+ before do
+ allow(view).to receive(:public_visibility_restricted?).and_return(false)
+ end
+
+ it { is_expected.to have_link(_('Explore'), href: explore_root_path) }
+ it { is_expected.to have_link(_('Help'), href: help_path) }
+ end
+
+ it { is_expected.to have_link(_('About GitLab'), href: "https://#{ApplicationHelper.promo_host}") }
+ it { is_expected.to have_link(_('Community forum'), href: ApplicationHelper.community_forum) }
+
+ context 'when one trust is enabled' do
+ before do
+ allow(view).to receive(:one_trust_enabled?).and_return(true)
+ end
+
+ it { is_expected.to have_button(_('Cookie Preferences'), class: 'ot-sdk-show-settings') }
+ end
+
+ context 'when one trust is disabled' do
+ before do
+ allow(view).to receive(:one_trust_enabled?).and_return(false)
+ end
+
+ it { is_expected.not_to have_button(_('Cookie Preferences'), class: 'ot-sdk-show-settings') }
+ end
+
+ it { is_expected.to have_css('.js-language-switcher') }
+end