From d8a3221aa3f9488c8c0ce4a1c6969c280680f673 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 3 Sep 2024 15:10:58 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .eslintrc.yml | 28 +-- .gitlab/ci/review-apps/dast.gitlab-ci.yml | 106 ------------ .gitlab/ci/review-apps/main.gitlab-ci.yml | 1 - .gitlab/ci/test-on-cng/main.gitlab-ci.yml | 2 + Gemfile | 2 +- Gemfile.checksum | 2 +- Gemfile.lock | 5 +- .../catalog/components/list/empty_state.vue | 3 + .../lib/utils/datetime/timeago_utility.js | 2 +- app/assets/javascripts/lib/utils/regexp.js | 12 ++ .../settings/components/access_dropdown.vue | 99 +++++++---- .../protected_branch_create.js | 15 +- .../search/results/components/blob_body.vue | 12 +- .../search/results/components/blob_chunks.vue | 28 +++ .../search/results/components/blob_footer.vue | 13 ++ .../search/results/components/blob_header.vue | 70 +++++++- .../results/components/zoekt_blob_results.vue | 10 +- .../javascripts/search/results/constants.js | 3 + .../javascripts/search/results/tracking.js | 5 + .../javascripts/vue_shared/mixins/timeago.js | 2 +- app/assets/stylesheets/framework/flash.scss | 16 +- app/assets/stylesheets/framework/layout.scss | 18 +- .../stylesheets/framework/variables.scss | 30 ---- .../page_bundles/learn_gitlab.scss | 3 - app/assets/stylesheets/utilities.scss | 90 +++------- app/controllers/application_controller.rb | 21 +-- .../groups/settings/repository_controller.rb | 4 + app/controllers/profiles_controller.rb | 4 +- .../projects/settings/ci_cd_controller.rb | 2 + .../settings/repository_controller.rb | 2 + app/models/anti_abuse/reports/note.rb | 11 ++ app/models/application_setting.rb | 3 - app/models/ci/job_annotation.rb | 13 +- app/models/packages/conan/metadatum.rb | 7 +- app/policies/projects/branch_rule_policy.rb | 10 +- .../ci/parse_annotations_artifact_service.rb | 3 +- .../json_schemas/member_role_permissions.json | 3 + app/views/layouts/_page.html.haml | 25 ++- ..._inactive_project_deletion_alert.html.haml | 2 +- config/application.rb | 1 - .../click_blob_results_show_more_less.yml | 23 +++ ...board_button_in_multimatch_file_header.yml | 18 ++ .../click_header_link_of_blob_result.yml | 18 ++ .../click_search_blob_result_blame_line.yml | 23 +++ .../events/click_search_blob_result_line.yml | 23 +++ .../beta/controller_static_context.yml | 9 - ...ck_blob_results_show_more_less_monthly.yml | 22 +++ ...tton_in_multimatch_file_header_monthly.yml | 22 +++ ...ick_header_link_of_blob_result_monthly.yml | 22 +++ ..._search_blob_result_blame_line_monthly.yml | 22 +++ ..._click_search_blob_result_line_monthly.yml | 22 +++ ...ick_blob_results_show_more_less_weekly.yml | 22 +++ ...utton_in_multimatch_file_header_weekly.yml | 22 +++ ...lick_header_link_of_blob_result_weekly.yml | 22 +++ ...k_search_blob_result_blame_line_weekly.yml | 22 +++ ...m_click_search_blob_result_line_weekly.yml | 22 +++ config/routes/profile.rb | 1 + config/sidekiq_queues.yml | 2 + ..._add_project_id_to_p_ci_job_annotations.rb | 9 + ...ndex_p_ci_job_annotations_on_project_id.rb | 19 +++ ..._annotations_project_id_null_constraint.rb | 22 +++ ..._null_project_ci_job_annotation_records.rb | 24 +++ ...b_annotation_project_id_null_constraint.rb | 27 +++ db/schema_migrations/20240826072312 | 1 + db/schema_migrations/20240826072410 | 1 + db/schema_migrations/20240826080618 | 1 + db/schema_migrations/20240826081110 | 1 + db/schema_migrations/20240902080505 | 1 + db/structure.sql | 4 + doc/api/api_resources.md | 1 + doc/api/graphql/reference/index.md | 2 + doc/api/openapi/openapi_v2.yaml | 24 +++ doc/api/personal_access_tokens.md | 80 +++++++++ doc/api/search.md | 3 + doc/api/web_commits.md | 46 +++++ doc/development/ai_features/index.md | 32 ++++ doc/integration/exact_code_search/zoekt.md | 27 +++ doc/integration/partner_marketplace.md | 161 +----------------- doc/user/custom_roles/abilities.md | 1 + doc/user/free_user_limit.md | 81 +++++---- doc/user/gitlab_duo/gateway.md | 81 +++++++++ .../repository/code_suggestions/index.md | 1 + .../repository/signed_commits/index.md | 8 +- doc/user/search/exact_code_search.md | 3 +- doc/user/search/index.md | 14 +- lib/api/api.rb | 1 + lib/api/entities/group_association_details.rb | 16 ++ .../personal_access_token_associations.rb | 10 ++ .../entities/project_association_details.rb | 21 +++ lib/api/helpers/protected_branches_helpers.rb | 12 ++ .../self_information.rb | 38 ++++- lib/api/protected_branches.rb | 10 +- lib/api/search.rb | 1 - lib/api/web_commits.rb | 57 +++++++ lib/gitaly/server.rb | 46 +++-- lib/gitlab/git/repository.rb | 4 +- lib/gitlab/gitaly_client.rb | 1 + lib/gitlab/gitaly_client/ref_service.rb | 6 +- lib/gitlab/gitaly_client/server_service.rb | 4 + .../action_controller_static_context.rb | 2 - lib/tasks/gitlab/keep_around.rake | 142 +++++++++++++++ locale/gitlab.pot | 9 + package.json | 2 +- qa/gems/gitlab-cng/exe/cng | 2 + .../lib/gitlab/cng/commands/_command.rb | 2 + .../lib/gitlab/cng/lib/helpers/output.rb | 25 ++- .../gitlab-cng/spec/integration/cng_spec.rb | 1 + spec/db/schema_spec.rb | 2 +- spec/factories/ci/job_annotations.rb | 2 + .../merge_request/user_views_diffs_spec.rb | 4 +- spec/features/protected_branches_spec.rb | 153 +---------------- .../components/list/empty_state_spec.js | 1 + spec/frontend/lib/utils/regexp_spec.js | 28 +++ .../components/access_dropdown_spec.js | 24 +++ .../protected_branch_create_spec.js | 1 + .../protected_branch_edit_spec.js | 1 + .../zoekt_blob_results_spec.js.snap | 4 + .../results/components/blob_body_spec.js | 2 + .../results/components/blob_chunks_spec.js | 28 ++- .../results/components/blob_footer_spec.js | 30 ++++ .../results/components/blob_header_spec.js | 39 +++++ .../components/zoekt_blob_results_spec.js | 2 +- .../components/user_lists_table_spec.js | 3 +- spec/lib/gitaly/server_spec.rb | 32 ++++ .../gitaly_client/server_service_spec.rb | 20 +++ .../action_controller_static_context_spec.rb | 26 +-- spec/lib/marginalia_spec.rb | 5 +- ..._project_ci_job_annotation_records_spec.rb | 40 +++++ spec/models/anti_abuse/reports/note_spec.rb | 8 +- spec/models/ci/job_annotation_spec.rb | 13 +- spec/models/packages/conan/metadatum_spec.rb | 37 ---- .../self_information_spec.rb | 47 +++++ spec/requests/api/search_spec.rb | 4 +- spec/requests/api/web_commits_spec.rb | 77 +++++++++ spec/requests/application_controller_spec.rb | 119 +++++-------- spec/support/capybara.rb | 2 + spec/support/helpers/graphql_helpers.rb | 2 +- .../protected_branches_shared_examples.rb | 111 ++++++++++++ spec/tasks/gitlab/keep_around_rake_spec.rb | 159 +++++++++++++++++ yarn.lock | 8 +- 140 files changed, 2244 insertions(+), 902 deletions(-) delete mode 100644 .gitlab/ci/review-apps/dast.gitlab-ci.yml create mode 100644 app/assets/javascripts/search/results/tracking.js delete mode 100644 app/assets/stylesheets/page_bundles/learn_gitlab.scss create mode 100644 config/events/click_blob_results_show_more_less.yml create mode 100644 config/events/click_clipboard_button_in_multimatch_file_header.yml create mode 100644 config/events/click_header_link_of_blob_result.yml create mode 100644 config/events/click_search_blob_result_blame_line.yml create mode 100644 config/events/click_search_blob_result_line.yml delete mode 100644 config/feature_flags/beta/controller_static_context.yml create mode 100644 config/metrics/counts_28d/count_distinct_user_id_from_click_blob_results_show_more_less_monthly.yml create mode 100644 config/metrics/counts_28d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_monthly.yml create mode 100644 config/metrics/counts_28d/count_distinct_user_id_from_click_header_link_of_blob_result_monthly.yml create mode 100644 config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_blame_line_monthly.yml create mode 100644 config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_line_monthly.yml create mode 100644 config/metrics/counts_7d/count_distinct_user_id_from_click_blob_results_show_more_less_weekly.yml create mode 100644 config/metrics/counts_7d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_weekly.yml create mode 100644 config/metrics/counts_7d/count_distinct_user_id_from_click_header_link_of_blob_result_weekly.yml create mode 100644 config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_blame_line_weekly.yml create mode 100644 config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_line_weekly.yml create mode 100644 db/migrate/20240826072312_add_project_id_to_p_ci_job_annotations.rb create mode 100644 db/post_migrate/20240826072410_index_p_ci_job_annotations_on_project_id.rb create mode 100644 db/post_migrate/20240826080618_add_p_ci_job_annotations_project_id_null_constraint.rb create mode 100644 db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records.rb create mode 100644 db/post_migrate/20240902080505_validate_p_ci_job_annotation_project_id_null_constraint.rb create mode 100644 db/schema_migrations/20240826072312 create mode 100644 db/schema_migrations/20240826072410 create mode 100644 db/schema_migrations/20240826080618 create mode 100644 db/schema_migrations/20240826081110 create mode 100644 db/schema_migrations/20240902080505 create mode 100644 doc/api/web_commits.md create mode 100644 doc/user/gitlab_duo/gateway.md create mode 100644 lib/api/entities/group_association_details.rb create mode 100644 lib/api/entities/personal_access_token_associations.rb create mode 100644 lib/api/entities/project_association_details.rb create mode 100644 lib/api/web_commits.rb create mode 100644 lib/tasks/gitlab/keep_around.rake create mode 100644 spec/frontend/lib/utils/regexp_spec.js create mode 100644 spec/lib/gitlab/gitaly_client/server_service_spec.rb create mode 100644 spec/migrations/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records_spec.rb create mode 100644 spec/requests/api/web_commits_spec.rb create mode 100644 spec/support/shared_examples/projects/protected_branches_shared_examples.rb create mode 100644 spec/tasks/gitlab/keep_around_rake_spec.rb diff --git a/.eslintrc.yml b/.eslintrc.yml index 20215977ae3..461c87b5dce 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,7 +3,7 @@ extends: - plugin:@gitlab/i18n - plugin:no-jquery/slim - plugin:no-jquery/deprecated-3.4 - - plugin:no-unsanitized/DOM + - plugin:no-unsanitized/recommended-legacy - ./tooling/eslint-config/conditionally_ignore.js globals: __webpack_public_path__: true @@ -164,11 +164,11 @@ rules: no-unsanitized/method: - error - escape: - methods: 'sanitize' + methods: ['sanitize'] no-unsanitized/property: - error - escape: - methods: 'sanitize' + methods: ['sanitize'] # This rule will be enabled later. unicorn/no-array-callback-reference: off vue/no-undef-components: @@ -234,17 +234,17 @@ overrides: no-restricted-imports: - error - paths: - - name: mousetrap - message: 'Import { Mousetrap } from ~/lib/mousetrap instead.' - - name: vuex - message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.' - - name: '@sentry/browser' - message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead - - name: ~/locale - importNames: - - __ - - s__ - message: 'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest' + - name: mousetrap + message: 'Import { Mousetrap } from ~/lib/mousetrap instead.' + - name: vuex + message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.' + - name: '@sentry/browser' + message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead + - name: ~/locale + importNames: + - __ + - s__ + message: 'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest' - files: - 'config/**/*' - 'scripts/**/*' diff --git a/.gitlab/ci/review-apps/dast.gitlab-ci.yml b/.gitlab/ci/review-apps/dast.gitlab-ci.yml deleted file mode 100644 index d3019577ab4..00000000000 --- a/.gitlab/ci/review-apps/dast.gitlab-ci.yml +++ /dev/null @@ -1,106 +0,0 @@ -.dast_conf: - tags: - - prm - # For scheduling dast job - extends: - - .reports:rules:schedule-dast - image: - name: "${CI_TEMPLATE_REGISTRY_HOST}/security-products/dast:$DAST_VERSION" - resource_group: dast_scan - variables: - DAST_USERNAME_FIELD: "name:user[login]" - DAST_PASSWORD_FIELD: "name:user[password]" - DAST_SUBMIT_FIELD: "css:.js-sign-in-button" - DAST_FULL_SCAN_ENABLED: "true" - DAST_VERSION: 3 - GIT_STRATEGY: none - # -Xmx is used to set the JVM memory to 6GB to prevent DAST OutOfMemoryError. - DAST_ZAP_CLI_OPTIONS: "-Xmx6144m" - before_script: - - 'export DAST_WEBSITE="${DAST_WEBSITE:-$(cat environment_url.txt)}"' - - 'export DAST_AUTH_URL="${DAST_WEBSITE}/users/sign_in"' - - 'export DAST_PASSWORD="${REVIEW_APPS_ROOT_PASSWORD}"' - # Help pages are excluded from scan as they are static pages. - # profile/two_factor_auth is excluded from scan to prevent 2FA from being turned on from user profile, which will reduce coverage. - - 'DAST_EXCLUDE_URLS="${DAST_WEBSITE}/help/.*,${DAST_WEBSITE}/-/profile/two_factor_auth,${DAST_WEBSITE}/users/sign_out"' - # Exclude the automatically generated monitoring project from being tested due to https://gitlab.com/gitlab-org/gitlab/-/issues/260362 - - 'export DAST_EXCLUDE_URLS="${DAST_EXCLUDE_URLS},${DAST_WEBSITE}/gitlab-instance-.*"' - needs: ["review-deploy"] - stage: dast - # Default job timeout set to 90m and dast rules needs 2h to so that it won't timeout. - timeout: 3h - # Add retry because of intermittent connection problems. See https://gitlab.com/gitlab-org/gitlab/-/issues/244313 - retry: 1 - artifacts: - paths: - - gl-dast-report.json # GitLab-specific - reports: - dast: gl-dast-report.json - expire_in: 1 week # GitLab-specific - allow_failure: true - -# DAST scan with a subset of Release scan rules. -# ZAP rule details can be found at https://www.zaproxy.org/docs/alerts/ - -dast:anti-clickjacking-header: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user1" - DAST_ONLY_INCLUDE_RULES: "10020" - script: - - /analyze - -dast:xss-persistant: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user2" - DAST_ONLY_INCLUDE_RULES: "40014" - script: - - /analyze - -dast:insecure-http-method: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user3" - DAST_ONLY_INCLUDE_RULES: "90028" - script: - - /analyze - -dast:server-side-template-inj: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user4" - DAST_ONLY_INCLUDE_RULES: "90035" - script: - - /analyze - -dast:server-side-template-inj-blind: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user5" - DAST_ONLY_INCLUDE_RULES: "90035" - script: - - /analyze - -dast:session-fixation: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user6" - DAST_ONLY_INCLUDE_RULES: "40013" - script: - - /analyze - -dast:xss-dombased: - extends: - - .dast_conf - variables: - DAST_USERNAME: "user10" - DAST_ONLY_INCLUDE_RULES: "40026" - script: - - /analyze diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 2e17e936dd8..d933a6bf89e 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -14,7 +14,6 @@ include: - local: .gitlab/ci/global.gitlab-ci.yml - local: .gitlab/ci/review-apps/rules.gitlab-ci.yml - local: .gitlab/ci/review-apps/qa.gitlab-ci.yml - - local: .gitlab/ci/review-apps/dast.gitlab-ci.yml - local: .gitlab/ci/review-apps/dast-api.gitlab-ci.yml .base-before_script: &base-before_script diff --git a/.gitlab/ci/test-on-cng/main.gitlab-ci.yml b/.gitlab/ci/test-on-cng/main.gitlab-ci.yml index 5b6a8eb113f..0239a4f0c6c 100644 --- a/.gitlab/ci/test-on-cng/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-cng/main.gitlab-ci.yml @@ -37,6 +37,8 @@ workflow: QA_CAN_TEST_PRAEFECT: "false" QA_ALLOW_LOCAL_REQUESTS: "true" QA_SUITE_STATUS_ENV_FILE: $CI_PROJECT_DIR/suite_status.env + # Force color output for cng orchestrator + CLICOLOR_FORCE: 1 # disable selective test execution until pipeline setup is implemented to support it correctly KNAPSACK_TEST_FILE_PATTERN: "" QA_TESTS: "" diff --git a/Gemfile b/Gemfile index bc19107282e..22e4d5900d7 100644 --- a/Gemfile +++ b/Gemfile @@ -155,7 +155,7 @@ gem 'grape-path-helpers', '~> 2.0.1', feature_category: :api gem 'rack-cors', '~> 2.0.1', require: 'rack/cors' # rubocop:todo Gemfile/MissingFeatureCategory # GraphQL API -gem 'graphql', '~> 2.3.5', feature_category: :api +gem 'graphql', '~> 2.3.14', feature_category: :api gem 'graphql-docs', '~> 5.0.0', group: [:development, :test], feature_category: :api gem 'graphiql-rails', '~> 1.10', feature_category: :api gem 'apollo_upload_server', '~> 2.1.6', feature_category: :api diff --git a/Gemfile.checksum b/Gemfile.checksum index 6923352bfde..ff82d5d804c 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -281,7 +281,7 @@ {"name":"graphiql-rails","version":"1.10.0","platform":"ruby","checksum":"b557f989a737c8b9e985142609bec52fb1e9393a701eb50e02a7c14422891040"}, {"name":"graphlient","version":"0.8.0","platform":"ruby","checksum":"98c408da1d083454e9f5e274f3b0b6261e2a0c2b5f2ed7b3ef9441d46f8e7cb1"}, {"name":"graphlyte","version":"1.0.0","platform":"ruby","checksum":"b5af4ab67dde6e961f00ea1c18f159f73b52ed11395bb4ece297fe628fa1804d"}, -{"name":"graphql","version":"2.3.5","platform":"ruby","checksum":"9c367835f86541660d24c3d81632267ecee553d304577aaee070f8ac05860af1"}, +{"name":"graphql","version":"2.3.14","platform":"ruby","checksum":"1781f33ab52f250f7bd6082f40ef15363d6acf98009b7acba70d54bee142f295"}, {"name":"graphql-client","version":"0.23.0","platform":"ruby","checksum":"f238b8e451676baad06bd15f95396e018192243dcf12c4e6d13fb41d9a2babc1"}, {"name":"graphql-docs","version":"5.0.0","platform":"ruby","checksum":"76baca6e5a803a4b6a9fbbbfdbf16742b7c4c546c8592b6e1a7aa4e79e562d04"}, {"name":"grpc","version":"1.63.0","platform":"aarch64-linux","checksum":"dc75c5fd570b819470781d9512105dddfdd11d984f38b8e60bb946f92d1f79ee"}, diff --git a/Gemfile.lock b/Gemfile.lock index 01fdafafd1d..493d3edee4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -906,8 +906,9 @@ GEM faraday (~> 2.0) graphql-client graphlyte (1.0.0) - graphql (2.3.5) + graphql (2.3.14) base64 + fiber-storage graphql-client (0.23.0) activesupport (>= 3.0) graphql (>= 1.13.0) @@ -2103,7 +2104,7 @@ DEPENDENCIES graphiql-rails (~> 1.10) graphlient (~> 0.8.0) graphlyte (~> 1.0.0) - graphql (~> 2.3.5) + graphql (~> 2.3.14) graphql-docs (~> 5.0.0) grpc (= 1.63.0) gssapi (~> 1.3.1) diff --git a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue index 0e341f722b5..c7907afc74d 100644 --- a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue +++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue @@ -2,12 +2,14 @@ import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url'; import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; +import EMPTY_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-catalog-md.svg'; import { s__ } from '~/locale'; import { COMPONENTS_DOCS_URL } from '~/ci/catalog/constants'; export default { name: 'CiCatalogEmptyState', COMPONENTS_DOCS_URL, + EMPTY_SVG_URL, components: { GlEmptyState, GlLink, @@ -77,6 +79,7 @@ export default { v-else :title="$options.i18n.default.title" :description="$options.i18n.default.description" + :svg-path="$options.EMPTY_SVG_URL" /> diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index 942802b944a..4f72127ec9b 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -120,7 +120,7 @@ export const getTimeago = (formatName) => export const localTimeAgo = (elements, updateTooltip = true) => { const { format } = getTimeago(); elements.forEach((el) => { - el.innerText = format(el.dateTime, timeagoLanguageCode); + el.innerText = format(newDate(el.dateTime), timeagoLanguageCode); }); if (!updateTooltip) { diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js index 240b871be18..82ceb73e23b 100644 --- a/app/assets/javascripts/lib/utils/regexp.js +++ b/app/assets/javascripts/lib/utils/regexp.js @@ -11,3 +11,15 @@ export const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; export const noSpacesRegex = /^\S+$/; + +/** + * Checks if a string contains potential regular expression elements. + * + * @param {string} str - The string to check for potential regex elements. + * @returns {boolean} - Returns true if the string contains regex elements, otherwise false. + */ + +export const containsPotentialRegex = (str) => { + const regexPattern = /[*+?^${}()|[\]\\]/; + return regexPattern.test(str); +}; diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue index 135e926cedf..0e75054a50d 100644 --- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue +++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue @@ -9,6 +9,7 @@ import { GlSprintf, } from '@gitlab/ui'; import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash'; +import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import { createAlert } from '~/alert'; import { __, s__, n__ } from '~/locale'; import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api'; @@ -35,6 +36,7 @@ export default { GlAvatar, GlSprintf, }, + mixins: [glAbilitiesMixin()], props: { accessLevelsData: { type: Array, @@ -187,6 +189,9 @@ export default { ...this.getDataForSave(LEVEL_TYPES.DEPLOY_KEY, 'deploy_key_id'), ]; }, + canAdminContainer() { + return this.glAbilities.adminProject || this.glAbilities.adminGroup; + }, }, watch: { query: debounce(function debouncedSearch() { @@ -226,29 +231,45 @@ export default { focusInput() { this.$refs.search?.focusInput(); }, + getGroups() { + return this.groups.length + ? Promise.resolve({ data: this.groups }) + : getGroups({ withProjectAccess: this.groupsWithProjectAccess }); + }, getData({ initial = false } = {}) { this.initialLoading = initial; this.loading = true; if (this.hasLicense) { - Promise.all([ - getDeployKeys(this.query), - getUsers(this.query), - this.groups.length - ? Promise.resolve({ data: this.groups }) - : getGroups({ withProjectAccess: this.groupsWithProjectAccess }), - ]) - .then(([deployKeysResponse, usersResponse, groupsResponse]) => { - this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data); - this.setSelected({ initial }); - }) - .catch(() => - createAlert({ message: __('Failed to load groups, users and deploy keys.') }), - ) - .finally(() => { - this.initialLoading = false; - this.loading = false; - }); + if (this.canAdminContainer) { + Promise.all([getDeployKeys(this.query), getUsers(this.query), this.getGroups()]) + .then(([deployKeysResponse, usersResponse, groupsResponse]) => { + this.consolidateData( + deployKeysResponse.data, + usersResponse.data, + groupsResponse.data, + ); + this.setSelected({ initial }); + }) + .catch(() => + createAlert({ message: __('Failed to load groups, users and deploy keys.') }), + ) + .finally(() => { + this.initialLoading = false; + this.loading = false; + }); + } else if (this.glAbilities.adminProtectedBranch) { + Promise.all([getUsers(this.query), this.getGroups()]) + .then(([usersResponse, groupsResponse]) => { + this.consolidateData(null, usersResponse.data, groupsResponse.data); + this.setSelected({ initial }); + }) + .catch(() => createAlert({ message: __('Failed to load groups and users.') })) + .finally(() => { + this.initialLoading = false; + this.loading = false; + }); + } } else { getDeployKeys(this.query) .then((deployKeysResponse) => { @@ -284,27 +305,31 @@ export default { } } - this.deployKeys = deployKeysResponse.map((response) => { - const { - id, - fingerprint, - fingerprint_sha256: fingerprintSha256, - title, - owner: { avatar_url, name, username }, - } = response; + if (this.canAdminContainer) { + this.deployKeys = deployKeysResponse.map((response) => { + const { + id, + fingerprint, + fingerprint_sha256: fingerprintSha256, + title, + owner: { avatar_url, name, username }, + } = response; - const availableFingerprint = fingerprintSha256 || fingerprint; - const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`; + const availableFingerprint = fingerprintSha256 || fingerprint; + const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`; - return { - id, - title: title.concat(' ', shortFingerprint), - avatar_url, - fullname: name, - username, - type: LEVEL_TYPES.DEPLOY_KEY, - }; - }); + return { + id, + title: title.concat(' ', shortFingerprint), + avatar_url, + fullname: name, + username, + type: LEVEL_TYPES.DEPLOY_KEY, + }; + }); + } else { + this.deployKeys = []; + } }, setSelected({ initial } = {}) { if (initial) { diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 612f801300e..f7425642582 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -143,13 +143,26 @@ export default class ProtectedBranchCreate { }); } + createLimitedSuccessAlert() { + this.alert = createAlert({ + variant: VARIANT_SUCCESS, + containerSelector: '.js-alert-protected-branch-created-container', + message: s__('ProtectedBranch|Protected branch was sucessfully created'), + }); + } + showSuccessAlertIfNeeded() { if (!this.hasProtectedBranchSuccessAlert()) { return; } this.expandAndScroll(PROTECTED_BRANCHES_ANCHOR); - this.createSuccessAlert(); + if (gon.abilities.adminProject || gon.abilities.adminGroup) { + this.createSuccessAlert(); + } else { + this.createLimitedSuccessAlert(); + } + localStorage.removeItem(IS_PROTECTED_BRANCH_CREATED); } diff --git a/app/assets/javascripts/search/results/components/blob_body.vue b/app/assets/javascripts/search/results/components/blob_body.vue index 3cfa63cb4ae..ada9edfac5b 100644 --- a/app/assets/javascripts/search/results/components/blob_body.vue +++ b/app/assets/javascripts/search/results/components/blob_body.vue @@ -13,6 +13,10 @@ export default { type: Object, required: true, }, + position: { + type: Number, + required: true, + }, }, data() { return { @@ -40,6 +44,7 @@ export default { if (this.showMore) { return file.chunks; } + return file.chunks.slice(0, DEFAULT_SHOW_CHUNKS); }, }, @@ -53,7 +58,12 @@ export default { :key="`chunk${index}`" class="chunks-block gl-border-b gl-border-subtle last:gl-border-0" > - + diff --git a/app/assets/javascripts/search/results/components/blob_chunks.vue b/app/assets/javascripts/search/results/components/blob_chunks.vue index 218675b2957..b1d5e71375f 100644 --- a/app/assets/javascripts/search/results/components/blob_chunks.vue +++ b/app/assets/javascripts/search/results/components/blob_chunks.vue @@ -2,6 +2,13 @@ import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui'; import GlSafeHtmlDirective from '~/vue_shared/directives/safe_html'; import { s__ } from '~/locale'; +import { InternalEvents } from '~/tracking'; +import { + EVENT_CLICK_BLOB_RESULT_LINE, + EVENT_CLICK_BLOB_RESULT_BLAME_LINE, +} from '~/search/results/tracking'; + +const trackingMixin = InternalEvents.mixin(); export default { name: 'BlobChunks', @@ -13,6 +20,7 @@ export default { GlTooltip: GlTooltipDirective, SafeHtml: GlSafeHtmlDirective, }, + mixins: [trackingMixin], i18n: { viewBlame: s__('GlobalSearch|View blame'), viewLine: s__('GlobalSearch|View line in repository'), @@ -32,6 +40,10 @@ export default { required: false, default: '', }, + position: { + type: Number, + required: true, + }, }, computed: { codeTheme() { @@ -42,6 +54,18 @@ export default { highlightedRichText(richText) { return richText.replace('', ''); }, + trackLineClick(lineNumber) { + this.trackEvent(EVENT_CLICK_BLOB_RESULT_LINE, { + property: lineNumber, + value: this.position, + }); + }, + trackBlameClick(lineNumber) { + this.trackEvent(EVENT_CLICK_BLOB_RESULT_BLAME_LINE, { + property: lineNumber, + value: this.position, + }); + }, }, }; @@ -63,6 +87,8 @@ export default { :href="`${blameLink}#L${line.lineNumber}`" :title="$options.i18n.viewBlame" class="js-navigation-open" + data-testid="search-blob-line-blame-link" + @click="trackBlameClick(line.lineNumber)" > @@ -72,6 +98,8 @@ export default { :href="`${fileUrl}#L${line.lineNumber}`" :title="$options.i18n.viewLine" class="!gl-flex gl-items-center gl-justify-end" + data-testid="search-blob-line-link" + @click="trackLineClick(line.lineNumber)" >{{ line.lineNumber }} diff --git a/app/assets/javascripts/search/results/components/blob_footer.vue b/app/assets/javascripts/search/results/components/blob_footer.vue index 8ac8b6856a1..0b83ba9820a 100644 --- a/app/assets/javascripts/search/results/components/blob_footer.vue +++ b/app/assets/javascripts/search/results/components/blob_footer.vue @@ -2,8 +2,12 @@ import { GlSprintf, GlButton, GlLink } from '@gitlab/ui'; import { s__ } from '~/locale'; import { DEFAULT_FETCH_CHUNKS, DEFAULT_SHOW_CHUNKS } from '~/search/results/constants'; +import { EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS } from '~/search/results/tracking'; +import { InternalEvents } from '~/tracking'; import eventHub from '../event_hub'; +const trackingMixin = InternalEvents.mixin(); + export default { name: 'BlobFooter', components: { @@ -11,6 +15,7 @@ export default { GlButton, GlLink, }, + mixins: [trackingMixin], i18n: { showMore: s__('GlobalSearch|Show %{matches} more matches'), showLess: s__('GlobalSearch|Show less'), @@ -23,6 +28,10 @@ export default { type: Object, required: true, }, + position: { + type: Number, + required: true, + }, }, data() { return { @@ -68,6 +77,10 @@ export default { id: `${this.projectPath}:${this.filePath}`, state: (this.showMore = !this.showMore), }); + this.trackEvent(EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS, { + label: `${this.position}`, + property: this.showMore ? 'open' : 'close', + }); }, }, DEFAULT_FETCH_CHUNKS, diff --git a/app/assets/javascripts/search/results/components/blob_header.vue b/app/assets/javascripts/search/results/components/blob_header.vue index 3f0ef1f69cf..30a11028bd0 100644 --- a/app/assets/javascripts/search/results/components/blob_header.vue +++ b/app/assets/javascripts/search/results/components/blob_header.vue @@ -1,8 +1,17 @@ diff --git a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue index a4e18a31532..9c1905e7b07 100644 --- a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue +++ b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue @@ -64,6 +64,9 @@ export default { projectPathAndFilePath({ projectPath = '', path = '' }) { return `${projectPath}:${path}`; }, + position(index) { + return index + 1; + }, }, }; @@ -73,7 +76,7 @@ export default {
- +
diff --git a/app/assets/javascripts/search/results/constants.js b/app/assets/javascripts/search/results/constants.js index e444679b2cc..62c536d269e 100644 --- a/app/assets/javascripts/search/results/constants.js +++ b/app/assets/javascripts/search/results/constants.js @@ -5,3 +5,6 @@ export const SEARCH_RESULTS_DEBOUNCE = 500; export const DEFAULT_SHOW_CHUNKS = 3; export const REF_FIELD_NAME = 'repository_ref'; + +export const DEFAULT_THEME_COLOR = 'white'; +export const DEFAULT_HEADER_LABEL_COLOR = '#D9C2EE'; diff --git a/app/assets/javascripts/search/results/tracking.js b/app/assets/javascripts/search/results/tracking.js new file mode 100644 index 00000000000..88b9a64e3d4 --- /dev/null +++ b/app/assets/javascripts/search/results/tracking.js @@ -0,0 +1,5 @@ +export const EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS = 'click_blob_results_show_more_less'; +export const EVENT_CLICK_BLOB_RESULT_BLAME_LINE = 'click_search_blob_result_blame_line'; +export const EVENT_CLICK_BLOB_RESULT_LINE = 'click_search_blob_result_line'; +export const EVENT_CLICK_CLIPBOARD_BUTTON = 'click_clipboard_button_in_multimatch_file_header'; +export const EVENT_CLICK_HEADER_LINK = 'click_header_link_of_blob_result'; diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js index ab27f197eb1..e957e517aec 100644 --- a/app/assets/javascripts/vue_shared/mixins/timeago.js +++ b/app/assets/javascripts/vue_shared/mixins/timeago.js @@ -13,7 +13,7 @@ export default { timeFormatted(time, format) { const timeago = getTimeago(format); - return timeago.format(time, timeagoLanguageCode); + return timeago.format(newDate(time), timeagoLanguageCode); }, tooltipTitle(time) { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index c2b42cb0e1a..a770af74929 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -2,22 +2,17 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); .flash-container { margin: 0; - margin-bottom: $gl-padding; font-size: 14px; position: relative; z-index: 1; + @apply gl-flex; + @apply gl-flex-col; + @apply gl-gap-3; &.sticky { position: sticky; top: $calc-application-header-height; z-index: 251; - - .flash-alert, - .flash-notice, - .flash-success, - .flash-warning { - margin-bottom: $gl-spacing-scale-4; - } } &.flash-container-page { @@ -63,11 +58,6 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); display: inline-block; } - .gl-alert { - margin-top: $gl-spacing-scale-4; - margin-bottom: $gl-spacing-scale-4; - } - &.flash-container-no-margin { .gl-alert { margin-top:0; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 62e35c19aa1..23dc6139459 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -50,17 +50,8 @@ html { } .alert-wrapper { - @include gl-media-breakpoint-up(xl) { - --gl-alert-padding-x: #{$gl-spacing-scale-3}; - --gl-broadcast-message-padding-x: #{$gl-spacing-scale-3}; - } - - .alert { - margin-bottom: 0; - - &:last-child { - margin-bottom: $gl-padding; - } + .gl-alert:first-child { + @apply gl-mt-3; } .alert-link-group { @@ -143,11 +134,6 @@ html { margin-bottom: 0; } - .alert-wrapper .flash-container .flash-alert:last-child, - .alert-wrapper .flash-container .flash-notice:last-child { - margin-bottom: 0; - } - .content-wrapper { padding-bottom: 0; flex: 1; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d5568e13956..efe31191cc9 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -61,27 +61,6 @@ $gl-spacing-scale-48: 48 * $grid-size; $gl-spacing-scale-75: 75 * $grid-size; /* End gitlab-ui#1709 */ -/* - * Why another sizing scale??? - * Great question, friend! - * This size scale is a "backport" of the equivalent set of "named" sizes - * (e.g. `xl` versus `70`) that came from the following design document as of 2019-10-23: - * - * https://gitlab-org.gitlab.io/gitlab-design/hosted/design-gitlab-specs/forms-spec-previews/ - * - * (See `input-` items at the bottom) - * - * The presumption here is that these sizes will be standardized in GitLab UI and thus will be - * broadly useful here in the GitLab product when not using the GitLab UI components. - */ -$size-scale: ( - 'xs': #{10 * $grid-size}, - 's': #{20 * $grid-size}, - 'm': #{30 * $grid-size}, - 'l': #{40 * $grid-size}, - 'xl': #{70 * $grid-size} -); - // Color schema $purple: #6d49cb !default; $purple-light: #ede8fb !default; @@ -115,15 +94,6 @@ $gl-font-size-16: 16px; $gl-font-size-28: 28px; $gl-font-size-42: 42px; -$type-scale: ( - 1: 12px, - 2: 14px, - 3: 16px, - 4: 20px, - 5: 28px, - 6: 42px -); - /* * Lists */ diff --git a/app/assets/stylesheets/page_bundles/learn_gitlab.scss b/app/assets/stylesheets/page_bundles/learn_gitlab.scss deleted file mode 100644 index 189aefb330b..00000000000 --- a/app/assets/stylesheets/page_bundles/learn_gitlab.scss +++ /dev/null @@ -1,3 +0,0 @@ -.learn-gitlab-info-card-content { - height: 200px; -} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 664e65c8602..4c1f260493a 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -4,24 +4,42 @@ to see the available utility classes. If you cannot find the class you need, consider adding it to Gitlab UI before adding it here. **/ + +$type-scale: ( + 1: 12px, + 2: 14px, + 4: 20px, +); + @each $index, $size in $type-scale { #{'.text-#{$index}'} { font-size: $size; } } +/* + * Why another sizing scale??? + * Great question, friend! + * This size scale is a "backport" of the equivalent set of "named" sizes + * (e.g. `xl` versus `70`) that came from the following design document as of 2019-10-23: + * + * https://gitlab-org.gitlab.io/gitlab-design/hosted/design-gitlab-specs/forms-spec-previews/ + * + * (See `input-` items at the bottom) + * + * The presumption here is that these sizes will be standardized in GitLab UI and thus will be + * broadly useful here in the GitLab product when not using the GitLab UI components. + */ + $size-scale: ( + 's': #{20 * $grid-size}, +); + @each $index, $size in $size-scale { #{'.mw-#{$index}'} { max-width: $size; } } -@each $index, $size in $type-scale { - #{'.lh-#{$index}'} { - line-height: $size; - } -} - @for $i from 1 through 12 { #{'.tab-width-#{$i}'} { /* stylelint-disable-next-line property-no-vendor-prefix */ @@ -30,13 +48,7 @@ } } -.border-width-1px { border-width: 1px; } -.border-style-dashed { border-style: dashed; } -.border-style-solid { border-style: solid; } -.border-color-blue-300 { border-color: $blue-300; } -.border-color-default { border-color: $border-color; } .border-radius-default { border-radius: $gl-border-radius-base; } -.border-radius-small { border-radius: $border-radius-small; } .box-shadow-default { box-shadow: 0 2px 4px 0 $t-gray-a-24; } // Override Bootstrap class with offset for system-header and @@ -50,34 +62,6 @@ top: $calc-application-header-height; } -// stylelint-disable-next-line gitlab/no-gl-class -.gl-children-ml-sm-3 > * { - @include media-breakpoint-up(sm) { - margin-left: $gl-spacing-scale-3; - } -} - -// stylelint-disable-next-line gitlab/no-gl-class -.gl-first-child-ml-sm-0 > a:first-child, -.gl-first-child-ml-sm-0 > button:first-child { - @include media-breakpoint-up(sm) { - margin-left: 0; - } -} - -.mh-50vh { max-height: 50vh; } - -.min-width-0 { - // By default flex items don't shrink below their minimum content size. To change this, set the item's min-width - min-width: 0; -} - -// stylelint-disable-next-line gitlab/no-gl-class -.gl-w-16 { width: px-to-rem($grid-size * 2); } -// stylelint-disable-next-line gitlab/no-gl-class -.gl-w-64 { width: px-to-rem($grid-size * 8); } -// stylelint-disable-next-line gitlab/no-gl-class -.gl-h-32 { height: px-to-rem($grid-size * 4); } // stylelint-disable-next-line gitlab/no-gl-class .gl-h-64 { height: px-to-rem($grid-size * 8); } @@ -100,25 +84,6 @@ display: flex; } -/** - Note: ::-webkit-scrollbar is a non-standard rule only - supported by webkit browsers. - - It is added here to migrate components that use - scrolling-links() mixin from `app/assets/stylesheets/framework/mixins.scss`. - - It should not be used elsewhere: it may impact accessibility as well as - add browser compatibility issues. - - See: https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar -**/ -// stylelint-disable-next-line gitlab/no-gl-class -.gl-webkit-scrollbar-display-none { - &::-webkit-scrollbar { - display: none; - } -} - // Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465 // stylelint-disable-next-line gitlab/no-gl-class .gl-focus-ring-border-1-gray-900\! { @@ -147,13 +112,6 @@ border-bottom-width: 0; } -// stylelint-disable-next-line gitlab/no-gl-class -.gl-md-h-9 { - @include gl-media-breakpoint-up(md) { - height: $gl-spacing-scale-9; - } -} - // stylelint-disable-next-line gitlab/no-gl-class .gl-pl-12 { padding-left: $gl-spacing-scale-12; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 714c2f14c0f..fa02be8f35c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -443,27 +443,18 @@ class ApplicationController < BaseActionController end def set_current_context(&block) - static_context = - if Feature.enabled?(:controller_static_context, Feature.current_request) - {} # middleware should've included caller_id and feature_category - else - { caller_id: self.class.endpoint_id_for_action(action_name) } - end - # even though feature_category is pre-populated by # Gitlab::Middleware::ActionControllerStaticContext # using the static annotation on controllers, the # controllers can override feature_category conditionally - static_context[:feature_category] = feature_category if feature_category.present? + Gitlab::ApplicationContext.push(feature_category: feature_category) if feature_category.present? Gitlab::ApplicationContext.push( - static_context.merge({ - user: -> { context_user }, - project: -> { @project if @project&.persisted? }, - namespace: -> { @group if @group&.persisted? }, - remote_ip: request.ip, - **http_router_rule_context - }) + user: -> { context_user }, + project: -> { @project if @project&.persisted? }, + namespace: -> { @group if @group&.persisted? }, + remote_ip: request.ip, + **http_router_rule_context ) yield ensure diff --git a/app/controllers/groups/settings/repository_controller.rb b/app/controllers/groups/settings/repository_controller.rb index ecd5d814fb6..6729ea1a014 100644 --- a/app/controllers/groups/settings/repository_controller.rb +++ b/app/controllers/groups/settings/repository_controller.rb @@ -9,6 +9,10 @@ module Groups before_action :authorize_access!, only: :show before_action :define_deploy_token_variables, if: -> { can?(current_user, :create_deploy_token, @group) } + before_action do + push_frontend_ability(ability: :admin_group, resource: @group, user: current_user) + end + feature_category :continuous_delivery urgency :low diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 0cbc8d25e67..9a050537676 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -11,7 +11,7 @@ class ProfilesController < Profiles::ApplicationController end feature_category :user_profile, [:reset_incoming_email_token, :reset_feed_token, - :reset_static_object_token, :update_username] + :reset_static_object_token, :update_username, :join_early_access_program] def reset_incoming_email_token Users::UpdateService.new(current_user, user: @user).execute! do |user| @@ -116,4 +116,4 @@ class ProfilesController < Profiles::ApplicationController end end -ProfilesController.prepend_mod +ProfilesController.prepend_mod_with('ProfilesController') diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 21a8da551c0..3157cfa8934 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -17,6 +17,8 @@ module Projects push_frontend_feature_flag(:ci_variables_pages, current_user) push_frontend_feature_flag(:allow_push_repository_for_job_token, @project) push_frontend_feature_flag(:ci_hidden_variables, @project.root_ancestor) + + push_frontend_ability(ability: :admin_project, resource: @project, user: current_user) end helper_method :highlight_badge diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index a06858cf9c1..ac57e1e59fa 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -9,6 +9,8 @@ module Projects before_action do push_frontend_feature_flag(:edit_branch_rules, @project) + push_frontend_ability(ability: :admin_project, resource: @project, user: current_user) + push_frontend_ability(ability: :admin_protected_branch, resource: @project, user: current_user) end feature_category :source_code_management, [:show, :cleanup, :update] diff --git a/app/models/anti_abuse/reports/note.rb b/app/models/anti_abuse/reports/note.rb index 7efd3b69bc5..19be2f9a65b 100644 --- a/app/models/anti_abuse/reports/note.rb +++ b/app/models/anti_abuse/reports/note.rb @@ -16,6 +16,12 @@ module AntiAbuse include ResolvableNote include Sortable + extend ::Gitlab::Utils::Override + + cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true + + redact_field :note + self.table_name = 'abuse_report_notes' belongs_to :abuse_report @@ -30,6 +36,11 @@ module AntiAbuse def discussion_class(_noteable = nil) AntiAbuse::Reports::IndividualNoteDiscussion end + + override :skip_project_check? + def skip_project_check? + true + end end end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5aafbf12f09..325ad80f397 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -8,9 +8,6 @@ class ApplicationSetting < ApplicationRecord include IgnorableColumns include Sanitizable - ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22' - ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21' - ignore_column :required_instance_ci_template, remove_with: '17.1', remove_after: '2024-05-10' ignore_column :sign_in_text_html, remove_with: '17.5', remove_after: '2024-10-17' ignore_columns %i[ encrypted_openai_api_key diff --git a/app/models/ci/job_annotation.rb b/app/models/ci/job_annotation.rb index f7380732f1b..03ed15f05c0 100644 --- a/app/models/ci/job_annotation.rb +++ b/app/models/ci/job_annotation.rb @@ -3,6 +3,9 @@ module Ci class JobAnnotation < Ci::ApplicationRecord include Ci::Partitionable + + before_validation :set_project_id, on: :create + include BulkInsertSafe self.table_name = :p_ci_job_annotations @@ -18,7 +21,13 @@ module Ci partitionable scope: :job, partitioned: true validates :data, json_schema: { filename: 'ci_job_annotation_data' } - validates :name, presence: true, - length: { maximum: 255 } + validates :name, presence: true, length: { maximum: 255 } + validates :project_id, presence: true, on: :create + + private + + def set_project_id + self.project_id ||= job&.project_id + end end end diff --git a/app/models/packages/conan/metadatum.rb b/app/models/packages/conan/metadatum.rb index 408707bf4c3..c9c9a127b08 100644 --- a/app/models/packages/conan/metadatum.rb +++ b/app/models/packages/conan/metadatum.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Packages::Conan::Metadatum < ApplicationRecord + include IgnorableColumns + + ignore_columns %i[os architecture build_type compiler compiler_version compiler_libcxx compiler_cppstd], + remove_with: '17.6', remove_after: '2024-10-22' NONE_VALUE = '_' belongs_to :package, class_name: 'Packages::Conan::Package', inverse_of: :conan_metadatum @@ -14,9 +18,6 @@ class Packages::Conan::Metadatum < ApplicationRecord validate :username_channel_none_values - validates :os, :architecture, :build_type, :compiler, :compiler_libcxx, :compiler_cppstd, length: { maximum: 32 } - validates :compiler_version, length: { maximum: 16 } - def recipe "#{package.name}/#{package.version}@#{package_username}/#{package_channel}" end diff --git a/app/policies/projects/branch_rule_policy.rb b/app/policies/projects/branch_rule_policy.rb index d1a6b66eebc..0094da66992 100644 --- a/app/policies/projects/branch_rule_policy.rb +++ b/app/policies/projects/branch_rule_policy.rb @@ -2,10 +2,12 @@ module Projects class BranchRulePolicy < ::ProtectedBranchPolicy - rule { can?(:read_protected_branch) }.enable :read_branch_rule - rule { can?(:create_protected_branch) }.enable :create_branch_rule - rule { can?(:update_protected_branch) }.enable :update_branch_rule - rule { can?(:destroy_protected_branch) }.enable :destroy_branch_rule + rule { can?(:admin_project) }.policy do + enable :read_branch_rule + enable :create_branch_rule + enable :update_branch_rule + enable :destroy_branch_rule + end end end diff --git a/app/services/ci/parse_annotations_artifact_service.rb b/app/services/ci/parse_annotations_artifact_service.rb index cbda7e827d4..e80fc11c0b3 100644 --- a/app/services/ci/parse_annotations_artifact_service.rb +++ b/app/services/ci/parse_annotations_artifact_service.rb @@ -36,7 +36,8 @@ module Ci raise ParserError, 'Annotations files must be a JSON object' unless blob_json.is_a?(Hash) blob_json.each do |key, value| - annotations.push(Ci::JobAnnotation.new(job: artifact.job, name: key, data: value)) + annotations.push(Ci::JobAnnotation.new(job: artifact.job, name: key, data: value, + project_id: project.id)) if annotations.size > annotations_num_limit raise SizeLimitError, diff --git a/app/validators/json_schemas/member_role_permissions.json b/app/validators/json_schemas/member_role_permissions.json index 86abb3ce9ce..2504dfc5df2 100644 --- a/app/validators/json_schemas/member_role_permissions.json +++ b/app/validators/json_schemas/member_role_permissions.json @@ -19,6 +19,9 @@ "admin_merge_request": { "type": "boolean" }, + "admin_protected_branch": { + "type": "boolean" + }, "admin_push_rules": { "type": "boolean" }, diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index dfdc983dfd4..d6f938b3762 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -2,6 +2,7 @@ -# Render the parent group sidebar while creating a new subgroup/project, see GroupsController#new. - group = @parent_group || @group - context = group || @project + - alert_class = "container-limited" unless fluid_layout - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user, organization: @organization) - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json @@ -11,7 +12,15 @@ .content-wrapper{ class: "#{@content_wrapper_class}" } = dispensable_render_if_exists 'layouts/header/verification_reminder' - .alert-wrapper.gl-force-block-formatting-context + + -# Broadcast messages + .broadcast-wrapper + = dispensable_render_if_exists "shared/token_expiration_banner" + = dispensable_render "layouts/broadcast" + = yield :group_invite_members_banner + + -# Alerts + .alert-wrapper.gl-flex.gl-flex-col.gl-gap-3.container-fluid{ class: alert_class } = dispensable_render 'shared/outdated_browser' = dispensable_render_if_exists "layouts/header/licensed_user_count_threshold" = dispensable_render_if_exists "layouts/header/token_expiry_notification" @@ -20,8 +29,6 @@ = dispensable_render_if_exists "shared/groups_projects/self_or_ancestor_marked_for_deletion_notice", context: context = dispensable_render "shared/projects/inactive_project_deletion_alert" = dispensable_render "shared/projects/archived_alert" - = dispensable_render_if_exists "shared/token_expiration_banner" - = dispensable_render "layouts/broadcast" = dispensable_render "layouts/header/read_only_banner" = dispensable_render "layouts/header/registration_enabled_callout" = dispensable_render "layouts/nav/classification_level_banner" @@ -35,13 +42,15 @@ = dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert" = dispensable_render_if_exists "shared/silent_mode_banner" = yield :page_level_alert - = yield :group_invite_members_banner - - unless @hide_top_bar - = render "layouts/nav/top_bar" + = render "layouts/flash" + = yield :after_flash_content + + -# Top bar + - unless @hide_top_bar + = render "layouts/nav/top_bar" + %div{ class: "#{container_class unless @no_container} #{@content_class}" } %main.content{ id: "content-body", **page_itemtype } - = render "layouts/flash", extra_flash_class: 'limit-container-width' - = yield :after_flash_content = yield :before_content = yield = yield :after_content diff --git a/app/views/shared/projects/_inactive_project_deletion_alert.html.haml b/app/views/shared/projects/_inactive_project_deletion_alert.html.haml index 0030265f007..8ac5c8b8def 100644 --- a/app/views/shared/projects/_inactive_project_deletion_alert.html.haml +++ b/app/views/shared/projects/_inactive_project_deletion_alert.html.haml @@ -4,4 +4,4 @@ - deletion_date = inactive_project_deletion_date(@project) - title = _('Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}').html_safe % { deletion_date: deletion_date, link_start: link_start, link_end: link_end } - = render Pajamas::AlertComponent.new(title: title, variant: :warning, alert_options: { class: 'gl-pb-3' }, dismissible: false) + = render Pajamas::AlertComponent.new(title: title, variant: :warning, dismissible: false) diff --git a/config/application.rb b/config/application.rb index 9fc590cdccf..27ce2d6cfa6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -328,7 +328,6 @@ module Gitlab config.assets.precompile << "page_bundles/issues_list.css" config.assets.precompile << "page_bundles/issues_show.css" config.assets.precompile << "page_bundles/jira_connect.css" - config.assets.precompile << "page_bundles/learn_gitlab.css" config.assets.precompile << "page_bundles/log_viewer.css" config.assets.precompile << "page_bundles/login.css" config.assets.precompile << "page_bundles/members.css" diff --git a/config/events/click_blob_results_show_more_less.yml b/config/events/click_blob_results_show_more_less.yml new file mode 100644 index 00000000000..81e7cfb3a5d --- /dev/null +++ b/config/events/click_blob_results_show_more_less.yml @@ -0,0 +1,23 @@ +--- +description: User clicks show more or show less button on the multimatch results page +internal_events: true +action: click_blob_results_show_more_less +identifiers: +- project +- namespace +- user +additional_properties: + value: + description: Position of the result. + property: + description: Set to 'open' if show more is activated and 'close' if it is closed again. +product_group: global_search +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/events/click_clipboard_button_in_multimatch_file_header.yml b/config/events/click_clipboard_button_in_multimatch_file_header.yml new file mode 100644 index 00000000000..743c0e59ef5 --- /dev/null +++ b/config/events/click_clipboard_button_in_multimatch_file_header.yml @@ -0,0 +1,18 @@ +--- +description: Click on copy to clipboard button +internal_events: true +action: click_clipboard_button_in_multimatch_file_header +identifiers: +- project +- namespace +- user +product_group: global_search +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/events/click_header_link_of_blob_result.yml b/config/events/click_header_link_of_blob_result.yml new file mode 100644 index 00000000000..a8eff68ca11 --- /dev/null +++ b/config/events/click_header_link_of_blob_result.yml @@ -0,0 +1,18 @@ +--- +description: Click on the header link of a file result on multimatch result page +internal_events: true +action: click_header_link_of_blob_result +identifiers: +- project +- namespace +- user +product_group: global_search +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/events/click_search_blob_result_blame_line.yml b/config/events/click_search_blob_result_blame_line.yml new file mode 100644 index 00000000000..f4562e95e46 --- /dev/null +++ b/config/events/click_search_blob_result_blame_line.yml @@ -0,0 +1,23 @@ +--- +description: This event tracks click on a blame link inside the multimatch result +internal_events: true +action: click_search_blob_result_blame_line +identifiers: +- project +- namespace +- user +additional_properties: + value: + description: Position of the result. + property: + description: Blame line. +product_group: global_search +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/events/click_search_blob_result_line.yml b/config/events/click_search_blob_result_line.yml new file mode 100644 index 00000000000..f4d38159380 --- /dev/null +++ b/config/events/click_search_blob_result_line.yml @@ -0,0 +1,23 @@ +--- +description: Click on line on the multimatch results page +internal_events: true +action: click_search_blob_result_line +identifiers: +- project +- namespace +- user +additional_properties: + value: + description: Position of the result. + property: + description: Line number. +product_group: global_search +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/feature_flags/beta/controller_static_context.yml b/config/feature_flags/beta/controller_static_context.yml deleted file mode 100644 index f78882a53da..00000000000 --- a/config/feature_flags/beta/controller_static_context.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: controller_static_context -feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/3593 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157628 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470047 -milestone: '17.4' -group: group::scalability -type: beta -default_enabled: false diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_blob_results_show_more_less_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_blob_results_show_more_less_monthly.yml new file mode 100644 index 00000000000..6efd4d1259a --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_blob_results_show_more_less_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_blob_results_show_more_less_monthly +description: Monthly count of unique users who click the show more or less button +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_blob_results_show_more_less + unique: user.id diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_monthly.yml new file mode 100644 index 00000000000..9dae08b8b9e --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_monthly +description: Monthly count of unique users who clicked copy to clipboard button in header of multimatch result +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_clipboard_button_in_multimatch_file_header + unique: user.id diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_header_link_of_blob_result_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_header_link_of_blob_result_monthly.yml new file mode 100644 index 00000000000..d1cbc211319 --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_header_link_of_blob_result_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_header_link_of_blob_result_monthly +description: Monthly count of unique users who clicked the blob result header link +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_header_link_of_blob_result + unique: user.id diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_blame_line_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_blame_line_monthly.yml new file mode 100644 index 00000000000..6b59eaed4d2 --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_blame_line_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_search_blob_result_blame_line_monthly +description: Monthly count of unique users who clicked on blame line in the multimatch results page +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_search_blob_result_blame_line + unique: user.id diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_line_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_line_monthly.yml new file mode 100644 index 00000000000..4c4cd755a2e --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_click_search_blob_result_line_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_search_blob_result_line_monthly +description: Monthly count of unique users who clicked on result line link +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_search_blob_result_line + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_blob_results_show_more_less_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_blob_results_show_more_less_weekly.yml new file mode 100644 index 00000000000..f8837b47366 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_blob_results_show_more_less_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_blob_results_show_more_less_weekly +description: Weekly count of unique users who click the show more or less button +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_blob_results_show_more_less + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_weekly.yml new file mode 100644 index 00000000000..b026dee81e3 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_clipboard_button_in_multimatch_file_header_weekly +description: Weekly count of unique users who clicked copy to clipboard button in header of multimatch result +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_clipboard_button_in_multimatch_file_header + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_header_link_of_blob_result_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_header_link_of_blob_result_weekly.yml new file mode 100644 index 00000000000..9a982ce3eb0 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_header_link_of_blob_result_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_header_link_of_blob_result_weekly +description: Weekly count of unique users who clicked the blob result header link +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_header_link_of_blob_result + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_blame_line_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_blame_line_weekly.yml new file mode 100644 index 00000000000..bfed523c5bf --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_blame_line_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_search_blob_result_blame_line_weekly +description: Weekly count of unique users who clicked on blame line in the multimatch results page +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_search_blob_result_blame_line + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_line_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_line_weekly.yml new file mode 100644 index 00000000000..0da8b049828 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_click_search_blob_result_line_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_click_search_blob_result_line_weekly +description: Weekly count of unique users who clicked on result line link +product_group: global_search +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.4' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161308 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: click_search_blob_result_line + unique: user.id diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 13642f429f6..f26fe25af88 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -12,6 +12,7 @@ resource :profile, only: [] do put :reset_feed_token put :reset_static_object_token put :update_username + post :join_early_access_program end scope module: :profiles do diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0ba0b02d36c..1765b8f9f7d 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -837,6 +837,8 @@ - 1 - - upload_checksum - 1 +- - users_experimental_communication_opt_in + - 1 - - users_record_last_activity - 1 - - users_track_namespace_visits diff --git a/db/migrate/20240826072312_add_project_id_to_p_ci_job_annotations.rb b/db/migrate/20240826072312_add_project_id_to_p_ci_job_annotations.rb new file mode 100644 index 00000000000..cbd478d0cd3 --- /dev/null +++ b/db/migrate/20240826072312_add_project_id_to_p_ci_job_annotations.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddProjectIdToPCiJobAnnotations < Gitlab::Database::Migration[2.2] + milestone '17.4' + + def change + add_column(:p_ci_job_annotations, :project_id, :bigint) + end +end diff --git a/db/post_migrate/20240826072410_index_p_ci_job_annotations_on_project_id.rb b/db/post_migrate/20240826072410_index_p_ci_job_annotations_on_project_id.rb new file mode 100644 index 00000000000..7444fee875a --- /dev/null +++ b/db/post_migrate/20240826072410_index_p_ci_job_annotations_on_project_id.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class IndexPCiJobAnnotationsOnProjectId < Gitlab::Database::Migration[2.2] + include Gitlab::Database::PartitioningMigrationHelpers + + milestone '17.4' + disable_ddl_transaction! + + TABLE_NAME = :p_ci_job_annotations + INDEX_NAME = :index_p_ci_job_annotations_on_project_id + + def up + add_concurrent_partitioned_index(TABLE_NAME, :project_id, name: INDEX_NAME) + end + + def down + remove_concurrent_partitioned_index_by_name(TABLE_NAME, INDEX_NAME) + end +end diff --git a/db/post_migrate/20240826080618_add_p_ci_job_annotations_project_id_null_constraint.rb b/db/post_migrate/20240826080618_add_p_ci_job_annotations_project_id_null_constraint.rb new file mode 100644 index 00000000000..aacfd91439c --- /dev/null +++ b/db/post_migrate/20240826080618_add_p_ci_job_annotations_project_id_null_constraint.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddPCiJobAnnotationsProjectIdNullConstraint < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.4' + + TABLE_NAME = :p_ci_job_annotations + COLUMN_NAME = :project_id + CONSTRAINT_NAME = :check_375bb9900a + + def up + Gitlab::Database::PostgresPartitionedTable.each_partition(TABLE_NAME) do |partition| + add_not_null_constraint(partition.identifier, COLUMN_NAME, constraint_name: CONSTRAINT_NAME, validate: false) + end + end + + def down + Gitlab::Database::PostgresPartitionedTable.each_partition(TABLE_NAME) do |partition| + remove_not_null_constraint(partition.identifier, COLUMN_NAME, constraint_name: CONSTRAINT_NAME) + end + end +end diff --git a/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records.rb b/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records.rb new file mode 100644 index 00000000000..779c628945b --- /dev/null +++ b/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class BackfillNullProjectCiJobAnnotationRecords < Gitlab::Database::Migration[2.2] + milestone '17.4' + restrict_gitlab_migration gitlab_schema: :gitlab_ci + disable_ddl_transaction! + + BATCH_SIZE = 1_000 + + def up + annotations_model = define_batchable_model('p_ci_job_annotations', primary_key: :id) + + annotations_model.each_batch(column: :id) do |batch| + batch + .where('p_ci_job_annotations.job_id = p_ci_builds.id') + .where('p_ci_job_annotations.partition_id = p_ci_builds.partition_id') + .update_all('project_id = p_ci_builds.project_id FROM p_ci_builds') + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20240902080505_validate_p_ci_job_annotation_project_id_null_constraint.rb b/db/post_migrate/20240902080505_validate_p_ci_job_annotation_project_id_null_constraint.rb new file mode 100644 index 00000000000..16a19ac778e --- /dev/null +++ b/db/post_migrate/20240902080505_validate_p_ci_job_annotation_project_id_null_constraint.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ValidatePCiJobAnnotationProjectIdNullConstraint < Gitlab::Database::Migration[2.2] + milestone '17.4' + + disable_ddl_transaction! + + TABLE_NAME = :p_ci_job_annotations + COLUMN_NAME = :project_id + CONSTRAINT_NAME = :check_375bb9900a + + def up + Gitlab::Database::PostgresPartitionedTable.each_partition(TABLE_NAME) do |partition| + add_not_null_constraint(partition.identifier, COLUMN_NAME, constraint_name: CONSTRAINT_NAME) + end + + add_not_null_constraint(TABLE_NAME, COLUMN_NAME, constraint_name: CONSTRAINT_NAME) + end + + def down + remove_not_null_constraint(TABLE_NAME, COLUMN_NAME, constraint_name: CONSTRAINT_NAME) + + Gitlab::Database::PostgresPartitionedTable.each_partition(TABLE_NAME) do |partition| + add_not_null_constraint(partition.identifier, COLUMN_NAME, constraint_name: CONSTRAINT_NAME, validate: false) + end + end +end diff --git a/db/schema_migrations/20240826072312 b/db/schema_migrations/20240826072312 new file mode 100644 index 00000000000..3dcfb86011c --- /dev/null +++ b/db/schema_migrations/20240826072312 @@ -0,0 +1 @@ +e359d298d6e198adc094e9c894ecfb2c80244d699eb27d8ff664269c0a3e9b21 \ No newline at end of file diff --git a/db/schema_migrations/20240826072410 b/db/schema_migrations/20240826072410 new file mode 100644 index 00000000000..8edca192efa --- /dev/null +++ b/db/schema_migrations/20240826072410 @@ -0,0 +1 @@ +1eb30f94ef39ce6c71efb80e5e068808958f9726f2282d09ed46ec42f6260b29 \ No newline at end of file diff --git a/db/schema_migrations/20240826080618 b/db/schema_migrations/20240826080618 new file mode 100644 index 00000000000..15ff9a2b482 --- /dev/null +++ b/db/schema_migrations/20240826080618 @@ -0,0 +1 @@ +fb89e621e0a9d87cd2b304ac9d95f131c881f16fb02f3863dc984d7199a0e35e \ No newline at end of file diff --git a/db/schema_migrations/20240826081110 b/db/schema_migrations/20240826081110 new file mode 100644 index 00000000000..0f8557d963d --- /dev/null +++ b/db/schema_migrations/20240826081110 @@ -0,0 +1 @@ +b692b0618940cbfac49b5f358e38613f5fbd799e1853362705f7539bb14d9a41 \ No newline at end of file diff --git a/db/schema_migrations/20240902080505 b/db/schema_migrations/20240902080505 new file mode 100644 index 00000000000..ca54f2ec15d --- /dev/null +++ b/db/schema_migrations/20240902080505 @@ -0,0 +1 @@ +771cadd0020345d7b52a97823f0b672465e0ab6c8f622fdee443162a73c79c30 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 93e2c5bc801..7a535a00a03 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2519,6 +2519,8 @@ CREATE TABLE p_ci_job_annotations ( job_id bigint NOT NULL, name text NOT NULL, data jsonb DEFAULT '[]'::jsonb NOT NULL, + project_id bigint, + CONSTRAINT check_375bb9900a CHECK ((project_id IS NOT NULL)), CONSTRAINT check_bac9224e45 CHECK ((char_length(name) <= 255)), CONSTRAINT data_is_array CHECK ((jsonb_typeof(data) = 'array'::text)) ) @@ -29283,6 +29285,8 @@ CREATE INDEX index_p_ci_finished_build_ch_sync_events_on_project_id ON ONLY p_ci CREATE UNIQUE INDEX index_p_ci_job_annotations_on_partition_id_job_id_name ON ONLY p_ci_job_annotations USING btree (partition_id, job_id, name); +CREATE INDEX index_p_ci_job_annotations_on_project_id ON ONLY p_ci_job_annotations USING btree (project_id); + CREATE INDEX index_p_ci_runner_machine_builds_on_runner_machine_id ON ONLY p_ci_runner_machine_builds USING btree (runner_machine_id); CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id); diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index d84ea0d0f6c..cacdff03143 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -205,6 +205,7 @@ The following API resources are available outside of project and group contexts | [Topics](topics.md) | `/topics` | | [Users](users.md) | `/users` | | [Validate `.gitlab-ci.yml` file](lint.md) | `/lint` | +| [Web commits](web_commits.md) | `/web_commits/public_key` | | [Version](version.md) | `/version` | ## Templates API resources diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 715a657d744..c0dd9bef439 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -34288,6 +34288,7 @@ Represents the location of a vulnerability found by a container security scan. | Name | Type | Description | | ---- | ---- | ----------- | +| `containerRepositoryUrl` | [`String`](#string) | URL of scanned image. | | `dependency` | [`VulnerableDependency`](#vulnerabledependency) | Dependency containing the vulnerability. | | `image` | [`String`](#string) | Name of the vulnerable container image. | | `operatingSystem` | [`String`](#string) | Operating system that runs on the vulnerable container image. | @@ -36899,6 +36900,7 @@ Member role permission. | `ADMIN_GROUP_MEMBER` | Add or remove users in a group, and assign roles to users. When assigning a role, users with this custom permission must select a role that has the same or fewer permissions as the default role used as the base for their custom role. | | `ADMIN_INTEGRATIONS` | Create, read, update, and delete integrations with external applications. | | `ADMIN_MERGE_REQUEST` | Allows approval of merge requests. | +| `ADMIN_PROTECTED_BRANCH` | Create, read, update, and delete protected branches for a project. | | `ADMIN_PUSH_RULES` | Configure push rules for repositories at the group or project level. | | `ADMIN_RUNNERS` | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | | `ADMIN_TERRAFORM_STATE` | Execute terraform commands, lock/unlock terraform state files, and remove file versions. | diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index f39368fe747..1218f5b8263 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -22,6 +22,8 @@ tags: description: Operations about groups - name: runners description: Operations about runners +- name: web_commits + description: Operations about web commits - name: group_avatar description: Operations about group_avatars - name: invitations @@ -39492,6 +39494,23 @@ paths: tags: - metadata operationId: getApiV4Version + "/api/v4/web_commits/public_key": + get: + summary: Get the GitLab public key for signing web commits. + description: This feature was introduced in GitLab 17.4. + tags: + - web_commits + produces: + - application/json + responses: + '200': + description: Get the GitLab public key for signing web commits. + schema: + "$ref": "#/definitions/API_Entities_Web_Commits" + '503': + description: Service unavailable + '404': + description: Not found "/api/v4/topics": get: summary: Get topics @@ -59883,6 +59902,11 @@ definitions: avatar_url: type: string description: API_Entities_Projects_Topic model + API_Entities_Web_Commits: + type: object + properties: + public_key: + type: string postApiV4Topics: type: object properties: diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md index 22a7f447bed..524c1c75505 100644 --- a/doc/api/personal_access_tokens.md +++ b/doc/api/personal_access_tokens.md @@ -383,6 +383,86 @@ curl --request DELETE --header "PRIVATE-TOKEN: " "https://git - `400: Bad Request` if not revoked successfully. - `401: Unauthorized` if the access token is invalid. +## List token associations + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/466046) in GitLab 17.4. + +Returns an unfiltered list of all groups, subgroups, and projects the current authenticated user can access. + +```plaintext +GET /personal_access_tokens/self/associations +GET /personal_access_tokens/self/associations?page=2 +GET /personal_access_tokens/self/associations?min_access_level=40 +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|---------------------|----------|----------|--------------------------------------------------------------------------| +| `min_access_level` | integer | No | Limit by current user minimal [role (`access_level`)](members.md#roles). | +| `page` | integer | No | Page to retrieve. Defaults to `1`. | +| `per_page` | integer | No | Number of records to return per page. Defaults to `20`. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/personal_access_tokens/self/associations" +``` + +Example response: + +```json +{ + "groups": [ + { + "id": 1, + "web_url": "http://gitlab.example.com/groups/test", + "name": "Test", + "parent_id": null, + "organization_id": 1, + "access_levels": 20, + "visibility": "public" + }, + { + "id": 3, + "web_url": "http://gitlab.example.com/groups/test/test_private", + "name": "Test Private", + "parent_id": 1, + "organization_id": 1, + "access_levels": 50, + "visibility": "test_private" + } + ], + "projects": [ + { + "id": 1337, + "description": "Leet.", + "name": "Test Project", + "name_with_namespace": "Test / Test Project", + "path": "test-project", + "path_with_namespace": "Test/test-project", + "created_at": "2024-07-02T13:37:00.123Z", + "access_levels": { + "project_access_level": null, + "group_access_level": 20 + }, + "visibility": "private", + "web_url": "http://gitlab.example.com/test/test_project", + "namespace": { + "id": 1, + "name": "Test", + "path": "Test", + "kind": "group", + "full_path": "Test", + "parent_id": null, + "avatar_url": null, + "web_url": "http://gitlab.example.com/groups/test" + } + } + ] +} +``` + ## Create a personal access token (administrator only) See the [Users API documentation](users.md#create-a-personal-access-token) for information on creating a personal access token. diff --git a/doc/api/search.md b/doc/api/search.md index ff044079e03..cec57e76352 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -21,6 +21,9 @@ these additional scopes are available for the [advanced search](#advanced-search - `blobs` - `notes` +If you want to use basic search instead, see +[specify a search type](../user/search/index.md#specify-a-search-type). + ## Advanced search API Search for a [term](../user/search/advanced_search.md#syntax) across the entire GitLab instance. diff --git a/doc/api/web_commits.md b/doc/api/web_commits.md new file mode 100644 index 00000000000..96f83e8cbee --- /dev/null +++ b/doc/api/web_commits.md @@ -0,0 +1,46 @@ +--- +stage: Create +group: Source Code +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Web Commits API + +DETAILS: +**Tier:** Free +**Offering:** GitLab.com, Self-managed, GitLab Dedicated + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/442533) in GitLab 17.4. + +Use this API to retrieve information about commits created with the Web UI. + +## Get public signing key + +Get the GitLab public key for signing web commits. + +```plaintext +GET /web_commits/public_key +``` + +If successful, returns [`200`](rest/index.md#status-codes) and the following +response attribute: + +| Attribute | Type | Description | +|--------------|--------|---------------------------------------------| +| `public_key` | string | GitLab public key for signing web commits. | + +Example request: + +```shell +curl --url "https://gitlab.example.com/api/v4/web_commits/public_key" +``` + +Example response: + +```json +[ + { + public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=\"" + } +] +``` diff --git a/doc/development/ai_features/index.md b/doc/development/ai_features/index.md index 51567d35c3e..869239ab5de 100644 --- a/doc/development/ai_features/index.md +++ b/doc/development/ai_features/index.md @@ -221,6 +221,38 @@ Apply the following feature flags to any AI feature work: See the [feature flag tracker epic](https://gitlab.com/groups/gitlab-org/-/epics/10524) for the list of all feature flags and how to use them. +### Push feature flags to AI Gateway + +You can push [feature flags](../feature_flags/index.md) to AI Gateway. This is helpful to gradually rollout user-facing changes even if the feature resides in AI Gateway. +See the following example: + +```ruby +# Push a feature flag state to AI Gateway. +Gitlab::AiGateway.push_feature_flag(:new_prompt_template, user) +``` + +Later, you can use the feature flag state in AI Gateway in the following way: + +```python +from ai_gateway.feature_flags import is_feature_enabled + +# Check if the feature flag "new_prompt_template" is enabled. +if is_feature_enabled('new_prompt_template'): + # Build a prompt from the new prompt template +else: + # Build a prompt from the old prompt template +``` + +**IMPORTANT:** At the [cleaning up](../feature_flags/controls.md#cleaning-up) step, remove the feature flag in AI Gateway repository **before** removing the flag in GitLab-Rails repository. +If you clean up the flag in GitLab-Rails repository at first, the feature flag in AI Gateway will be disabled immediately as it's the default state, hence you might encounter a surprising behavior. + +**IMPORTANT:** Cleaning up the feature flag in AI Gateway will immediately distribute the change to all GitLab instances, including GitLab.com, Self-managed GitLab, and Dedicated. + +Technical details: When `push_feature_flag` runs on an enabled feature flag, the name of flag is cached in the current context, +which is later attached to `x-gitlab-enabled-feature-flags` HTTP header when GitLab-Sidekiq/Rails requests to AI Gateway. + +As a simialr concept, we also have [`push_frontend_feature_flag`](../feature_flags/index.md) to push feature flags to frontend. + ### GraphQL API To connect to the AI provider API using the Abstraction Layer, use an extendable diff --git a/doc/integration/exact_code_search/zoekt.md b/doc/integration/exact_code_search/zoekt.md index 6e6fb5351eb..8d8eb7f319c 100644 --- a/doc/integration/exact_code_search/zoekt.md +++ b/doc/integration/exact_code_search/zoekt.md @@ -92,6 +92,33 @@ To pause indexing for [exact code search](../../user/search/exact_code_search.md When you pause indexing for exact code search, all changes in your repository are queued. To resume indexing, clear the **Pause indexing for exact code search** checkbox. +## Control indexing concurrency + +Prerequisites: + +- You must have administrator access to the instance. + +You can set the number of concurrent indexing tasks +that can be run on a Zoekt node relative to its CPU capacity. + +A higher ratio allows more tasks to run concurrently, potentially +improving indexing throughput at the cost of increased CPU usage. +The default value is `1.0`, meaning one task per CPU core. + +You can adjust this value based on your Zoekt node's performance +characteristics and workload. + +To set the number of concurrent indexing tasks: + +1. On the left sidebar, at the bottom, select **Admin**. +1. Select **Settings > Search**. +1. Expand **Exact code search configuration**. +1. In the **Indexing CPU to tasks multiplier** text box, enter a value. + + For example, if a Zoekt node has `4` CPU cores and the ratio is set to `1.5`, the + number of concurrent tasks for a node is going to be `4 * 1.5`, which is `6`. +1. Select **Save changes**. + ## Troubleshooting When working with Zoekt, you might encounter the following issues. diff --git a/doc/integration/partner_marketplace.md b/doc/integration/partner_marketplace.md index ef6eb8bd22e..69f54de9de7 100644 --- a/doc/integration/partner_marketplace.md +++ b/doc/integration/partner_marketplace.md @@ -2,163 +2,14 @@ stage: Fulfillment group: Provision info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +remove_date: '2024-12-02' +redirect_to: 'index.md' --- -# Marketplace partners + -GitLab supports automation for selected distribution marketplaces to process sales of GitLab products to authorized -channel partners. Marketplace partners can use the GitLab Marketplace APIs to integrate their systems with GitLab to -sell GitLab subscriptions on their site. +# Marketplace partners (removed) -This document's target audience is third-party developers for Marketplace partners. +This feature was removed in GitLab 17.4. -## How the Marketplace APIs work - -The Marketplace APIs are hosted in the [Customers Portal](https://customers.gitlab.com/). The Customers Portal allows -individual customers to purchase and manage GitLab subscriptions and supports APIs for partners -to make sales on behalf of their customers. The Customers Portal integrates with other GitLab services, including -Zuora and Salesforce, to provide a task-oriented interface for users. - -The following example shows a typical purchase flow of request and response between the following components: - -- Customer -- Marketplace partner system -- Customers Portal -- Zuora -- Salesforce - -```mermaid -%%{init: { "fontFamily": "GitLab Sans" }}%% -sequenceDiagram -accTitle: Purchase flow -accDescr: Shows the flow of a purchase from the customer, through the customer portal, Zuora, and Salesforce. - - participant Customer - participant Marketplace partner system - participant Customers Portal - participant Zuora - participant Salesforce - Customer ->> Marketplace partner system: Place order to purchase GitLab subscription - Marketplace partner system ->> Customers Portal: Get OAuth token - Customers Portal ->> Marketplace partner system: Access token - Marketplace partner system ->> Customers Portal: Place order - Customers Portal ->> Zuora: Create Zuora subscription - Customers Portal ->> Salesforce: Create Salesforce objects - Zuora ->> Customers Portal: Success response with Zuora subscription data - Customers Portal ->> Marketplace partner system: Success response with order ID - Zuora ->> Customers Portal: Zuora callout event - Customers Portal ->> Customer: send license notification - Marketplace partner system ->> Customers Portal: Poll order status - Customers Portal ->> Marketplace partner system: Success response with order status -``` - -## Marketplace API Specification - -OpenAPI specs for the Marketplace APIs are available at [Marketplace interactive API documentation](https://customers.staging.gitlab.com/openapi_docs/marketplace). - -## Access the Marketplace API - -To access the Marketplace API you need to: - -- Request access from GitLab. -- Retrieve an OAuth access token. - -Marketplace API endpoints are secured with [OAuth 2.0](https://oauth.net/2/). OAuth is an authorization framework -that grants 3rd party or client applications, like a Marketplace partner application, limited access to resources on an -HTTP service, like the Customers Portal. - -OAuth 2.0 uses _grant types_ (or _flows_) that describe how a client application gets authorization in -the form of an _access token_. An access token is a string that the client application uses to make authorized requests to -the resource server. - -The Marketplace API uses the `client_credentials` grant type. The client application uses the access token to access its -own resources, instead of accessing resources on behalf of a user. - -### Step 1: Request access - -Before you can use the Marketplace API, you must contact your Marketplace partner Manager or email `partnerorderops@gitlab.com` -to request access. After you request access, GitLab configures the following accounts and credentials for you: - -1. Client credentials. Marketplace APIs are secured with OAuth 2.0. The client credentials include the client ID and client secret - that you need to retrieve the OAuth access token. -1. Invoice owner account in Zuora system. Required for invoice processing. -1. Distributor account in Salesforce system. -1. Trading partner account in Salesforce system. GitLab adds the Trading Partner ID to a permitted list to pass the validations. - -### Step 2: Retrieve an access token - -To retrieve an access token, - -- Make a POST request to the [`/oauth/token`](https://customers.staging.gitlab.com/openapi_docs/marketplace#/marketplace/post_oauth_token) endpoint with the following required parameters: - -| Parameter | Type | Required |Description | -|-----------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------| -| `client_id` | string | yes |ID of your client application record on the Customers Portal. Received from GitLab. | -| `client_secret` | string | yes |Secret of your client application record on the Customers Portal. Received from GitLab. | -| `grant_type` | string | yes |Specifies the type of credential flow. Use `client_credentials`. | -| `scope` | string | yes |Specifies the level of access. Use `marketplace.order:read` for read-only access. Use `marketplace.order:create` for create access. | - -If the request is successful, the response body includes the access token that you can use in subsequent requests. For an example of a successful -response, see the [Marketplace interactive API documentation](https://customers.staging.gitlab.com/openapi_docs/marketplace) - -If the request is unsuccessful, the response body includes an error and error description. The errors can be: - -| Status | Description | -|--------|----------------------------------------------------------------------------------------------------------------------------------------------| -| 400 | Invalid scope. Ensure the `scope` is `marketplace.order:read` or `marketplace.order:create`. | -| 401 | Invalid client. Ensure that there are no typos or extra spaces on the client specific credentials. Incorrect `client_id` or `client_secret` | - -### Step 3: Use the access token - -To use the access token from a client application: - -1. Set the `Authorization` header of the request to `Bearer `. -1. Set parameters or data needed for the endpoint and send the request. - -Example request: - -```shell -curl \ - --url "https://customers.staging.gitlab.com/api/v1/marketplace/subscriptions/:external_subscription_id" \ - --header "Authorization: Bearer NHb_VhZhPOnBTSNfBSzmCmt28lLDWb2xtwr_c3DL148" -``` - -## Create a new customer subscription - -To create a new customer subscription from a Marketplace partner client application, - -- Make an authorized POST request to the - [`/api/v1/marketplace/subscriptions`](https://customers.staging.gitlab.com/openapi_docs/marketplace#/marketplace/post_api_v1_marketplace_subscriptions) - endpoint in the Customers Portal with the following parameters in JSON format: - -| Parameter | Type | Required | Description | -|--------------------------|--------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| `externalSubscriptionId` | string | yes | ID of the subscription on the Marketplace partner system. | -| `tradingPartnerId` | string | yes | ID of the Trading Partner account in Salesforce. Received from GitLab. | -| `customer` | object | yes | Information about the customer. Must include company name. Contact must include `firstName`, `lastName` and `email`. Address must include `country`. | -| `orderLines` | array | yes | Specifies the product purchased. Must include `quantity` and `productId`. | - -If the request is successful, the response body includes the newly created subscription number. For an example of a full request body, -see the [Marketplace interactive API documentation](https://customers.staging.gitlab.com/openapi_docs/marketplace). - -If the subscription creation is unsuccessful, the response body includes an error message with details about the cause of the error. - -## Check the status of a subscription - -To get the status of a given subscription, - -- Make an authorized GET request to the - [`/api/v1/marketplace/subscriptions/{external_subscription_id}`](https://customers.staging.gitlab.com/openapi_docs/marketplace#/marketplace/get_api_v1_marketplace_subscriptions__external_subscription_id_) - endpoint in the Customers Portal. - -The request must include the Marketplace partner system ID of the subscription to fetch the status for. - -If the request is successful, the response body contains the status of the subscription provision. The status can be: - -- Creating -- Created -- Failed -- Provisioned - -If the subscription cannot be found using the passed `external_subscription_id`, the response has -a 404 Not Found status. + \ No newline at end of file diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md index 21157a17d1d..a9ffbd4252b 100644 --- a/doc/user/custom_roles/abilities.md +++ b/doc/user/custom_roles/abilities.md @@ -86,6 +86,7 @@ These requirements are documented in the `Required permission` column in the fol | Name | Required permission | Description | Introduced in | Feature flag | Enabled in | |:-----|:------------|:------------------|:---------|:--------------|:---------| | [`admin_merge_request`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) | | Allows approval of merge requests. | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/412708) | | | +| [`admin_protected_branch`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162208) | | Create, read, update, and delete protected branches for a project. | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/448823) | | | | [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | `custom_ability_admin_push_rules` | | | [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) | diff --git a/doc/user/free_user_limit.md b/doc/user/free_user_limit.md index 8da3e324410..b8229696aa4 100644 --- a/doc/user/free_user_limit.md +++ b/doc/user/free_user_limit.md @@ -85,55 +85,72 @@ Prerequisites: 1. On the left sidebar, select **Search or go to** and find your group. 1. Select **Settings > Usage Quotas**. 1. To view all members, select the **Seats** tab. -1. To remove a member, select **Remove user**. -If you need more time to manage your members, or to try GitLab features -with a team of more than five members, you can [start a trial](https://gitlab.com/-/trial_registrations/new?glm_source=docs.gitlab.com?&glm_content=free-user-limit-faq/ee/user/free_user_limit.html). -A trial lasts for 30 days and includes an unlimited number of members. +On this page, you can view and manage all members in your namespace. For example, +to remove a member, select **Remove user**. ## Include a group in an organization's subscription -If there are multiple groups in your organization, they might have a -combination of Paid and Free subscriptions. When a group -with a Free subscription exceeds the user limit, their namespace becomes [read-only](../user/read_only_namespaces.md). +If you have multiple groups in your organization, they might have a +combination of paid (Premium or Ultimate tier) and Free tier subscriptions. +When a group with a Free tier subscription exceeds the user limit, their +namespace becomes [read-only](../user/read_only_namespaces.md). -To avoid user limits on groups with Free subscriptions, you can -include them in your organization's subscription. To check if a group is included in the subscription, -[view the group's subscription details](../subscriptions/gitlab_com/index.md#view-your-gitlabcom-subscription). -If the group is on the Free tier, it is not included in your organization's subscription. +To remove user limits on groups with Free tier subscriptions, include those groups +in your organization's subscription: -To include the group in your Paid subscription, [transfer the group](../user/group/manage.md#transfer-a-group) to your organization's -top-level namespace. +1. To check if a group is included in the subscription, + [view that group's subscription details](../subscriptions/gitlab_com/index.md#view-your-gitlabcom-subscription). -NOTE: -If you previously purchased a subscription and the 5-user limit was applied to a group, -ensure that [your subscription is linked](../subscriptions/gitlab_com/index.md#change-the-linked-group) -to the correct top-level namespace, or that it has been -linked to your Customers Portal account. + If the group has a Free tier subscription, it is not included in your organization's + subscription. -### Impact on seat count by transferred groups +1. To include a group in your paid Premium or Ultimate tier subscription, + [transfer that group](../user/group/manage.md#transfer-a-group) to your + organization's top-level namespace. -When you transfer a group, there might be an increase in your seat count, -which could incur additional costs for your subscription. +If the five-user limit has been applied to your group even though you have +a paid subscription in the Premium or Ultimate tier, make sure that +[your subscription is linked](../subscriptions/gitlab_com/index.md#change-the-linked-group) +to either of the following: -For example, a company has Group A and Group B: +- The correct top-level namespace. +- Your [Customers Portal](../subscriptions/customers_portal.md) account. -- Group A is on a Paid tier and has five users. -- Group B is on the Free tier and has eight users, four of which are members of Group A. -- Group B is placed in a read-only state when it exceeds the user limit. -- Group B is transferred to the company's subscription to remove the read-only state. -- The company incurs an additional cost of four seats for the +### Impact of transferred groups on subscription costs + +When you transfer a group to your organization's subscription, this might +increase your seat count. This could incur additional costs for your subscription. + +For example, your company has Group A and Group B: + +- Group A has a paid Premium or Ultimate tier subscription and has five users. +- Group B has a Free tier subscription and has eight users, four of which are + members of Group A. +- Group B is a read-only state because it exceeds the five-user limit. +- You transfer Group B to your company's subscription to remove the read-only state. +- Your company incurs an additional cost of four seats for the four members of Group B that are not members of Group A. -Users that are not part of the top-level namespace require additional seats to remain active. For more information, see [Add seats to your subscription](../subscriptions/gitlab_com/index.md#add-seats-to-your-subscription). +Users that are not part of the top-level namespace require additional seats to +remain active. For more information, see +[add seats to your subscription](../subscriptions/gitlab_com/index.md#add-seats-to-your-subscription). ## Increase the five-user limit -On the Free tier on GitLab.com, you cannot increase the limit of five users on top-level groups with private visibility. +On the Free subscription tier on GitLab.com, you cannot increase the limit of five users on +top-level groups with private visibility. -For larger teams, you should upgrade to the Premium or Ultimate tier, which -has no user limits and offers more features to increase team productivity. To experience the -value of Paid features and unlimited users, you should start a [free trial](https://gitlab.com/-/trial_registrations/new?glm_source=docs.gitlab.com/ee/user/free_user_limit.html) for GitLab Ultimate. +For larger teams, you should upgrade to the paid Premium or Ultimate tiers. These tiers +do not limit users and have more features to increase team productivity. For more +information, see: + +- [Upgrade your subscription tier on GitLab self-managed](../subscriptions/self_managed/index.md#upgrade-your-subscription-tier). +- [Upgrade your subscription tier on GitLab.com](../subscriptions/gitlab_com/index.md#upgrade-your-gitlabcom-subscription-tier). + +To try the paid tiers before deciding to upgrade, start a +[free trial](https://gitlab.com/-/trial_registrations/new?glm_source=docs.gitlab.com/ee/user/free_user_limit.html) +for GitLab Ultimate. ## Manage members in personal projects outside a group namespace diff --git a/doc/user/gitlab_duo/gateway.md b/doc/user/gitlab_duo/gateway.md new file mode 100644 index 00000000000..173e45679cc --- /dev/null +++ b/doc/user/gitlab_duo/gateway.md @@ -0,0 +1,81 @@ +--- +stage: AI-powered +group: AI Model Validation +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# AI gateway + +The [AI gateway](../../architecture/blueprints/ai_gateway/index.md) is a standalone service that gives access to AI-powered GitLab Duo features. + +GitLab operates an instance of *AI Gateway* that is used by all GitLab instances, including self-managed, GitLab Dedicated, and GitLab.com via [Cloud Connector](../../development/cloud_connector/index.md). + +This page describes where the AI gateway is deployed, and answers questions about region selection, data routing, and data sovereignty. + +## Region support + +For self-managed and Dedicated customers, the ability to choose the region is planned for future implementation. Currently, the process for region selection is managed internally by GitLab. + +Runway, is currently not available to external customers. GitLab is working on expanding support to include self-managed instances in the future (Epic: [Expand Platform Engineering to more runtimes](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/1330)). + +[View the available regions](https://gitlab-com.gitlab.io/gl-infra/platform/runway/runwayctl/manifest.schema.html#spec_regions). + +For GitLab.com customers, it's important to note that the current routing mechanism is based on the location of the GitLab instance, not the user's location. As GitLab.com is currently single-homed in `us-east1`, requests to the AI gateway are routed to us-east4 in almost all cases. This means that the routing may not always result in the absolute nearest deployment for every user. + +GitLab is working on an initiative to bypass the monolith when communicating with the AI Gateway (Epic: [Let the client (IDE) request Code Suggestions](https://gitlab.com/groups/gitlab-org/-/epics/13252)). This effort aims to improve routing efficiency and potentially allow for more user-location-based routing in the future. + +### Automatic routing + +GitLab leverages Cloudflare and *Google Cloud Platform* (GCP) load balancers to route AI +gateway requests to the nearest available deployment automatically. This routing +mechanism prioritizes low latency and efficient processing of user requests. + +You cannot manually control this routing process. The system dynamically selects the optimal region based on factors like network conditions and server load. + +### Tracing requests to specific regions + +You cannot directly trace your AI requests to specific regions at this time. + +If you need assistance with tracing a particular request, GitLab Support can access and +analyze logs that contain Cloudflare headers and instance UUIDs. These logs provide +insights into the routing path and can help identify the region where a request was processed. + +## Data sovereignty + +It's important to acknowledge the current limitations regarding strict data sovereignty enforcement in our multi-region AI gateway deployment. Currently, we cannot guarantee requests will go to or remain within a particular region and therefore is not a data residency solution. + +### Factors that influence data routing + +The following factors influence where data is routed. + +- **Network latency:** The primary routing mechanism focuses on minimizing latency, meaning data might be processed in a region other than the nearest one if network conditions dictate. +- **Service availability:** In case of regional outages or service disruptions, requests might be automatically rerouted to ensure uninterrupted service. +- **Third-Party dependencies:** The GitLab AI infrastructure relies on third-party model providers, like Google Vertex AI, which have their own data handling practices. + +### AI-gateway deployment regions + +For the most up-to-date information on AI gateway deployment regions, please refer to the [AI-assist runway configuration file](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/.runway/runway.yml?ref_type=heads#L12). + +As of the last update (2023-11-21), GitLab deploys the AI gateway in the following regions: + +- North America (`us-east4`) +- Europe (`europe-west2`, `europe-west3`, `europe-west9`) +- Asia Pacific (`asia-northeast1`, `asia-northeast3`) + +Please note that deployment regions may change frequently. For the most current information, always check the configuration file linked above. + +The exact location of the LLM models used by the AI gateway is determined by the third-party model providers. Currently, there is no guarantee that the models reside in the same geographical regions as the AI gateway deployments. This implies that data may flow back to the US or other regions where the model provider operates, even if the AI-gateway processes the initial request in a different region. + +### Data Flow and LLM model locations + +GitLab is working closely with LLM providers to understand their regional data handling practices fully. Currently, there might be instances where data is transmitted to regions outside the one closest to the user due to the factors mentioned above. + +### Future enhancements + +GitLab is actively working to let customers specify data residency requirements more granularly in the future. The proposed functionality can provide greater control over data processing locations and help meet specific compliance needs. + +## Specific regional questions + +### Data routing post-Brexit + +The UK's exit from the EU does not directly impact data routing preferences or decisions for AI gateway. Data will continue to be routed to the most optimal region based on performance and availability. Data can still flow freely between the EU and UK. diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md index 99d9330db1f..7950d34c470 100644 --- a/doc/user/project/repository/code_suggestions/index.md +++ b/doc/user/project/repository/code_suggestions/index.md @@ -119,6 +119,7 @@ For use cases and best practices, follow the [GitLab Duo examples documentation] > - [Introduced](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/issues/276) in GitLab VS Code Extension 4.20.0. > - [Introduced](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin/-/issues/462) in GitLab Duo for JetBrains 2.7.0. > - [Added](https://gitlab.com/gitlab-org/editor-extensions/gitlab.vim/-/merge_requests/152) to the GitLab Neovim plugin on July 16, 2024. +> - Feature flags `advanced_context_resolver` and `code_suggestions_context` [enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161538) in GitLab 17.4. FLAG: The availability of this feature is controlled by a feature flag. diff --git a/doc/user/project/repository/signed_commits/index.md b/doc/user/project/repository/signed_commits/index.md index 614779a4e20..ac2a8dbe806 100644 --- a/doc/user/project/repository/signed_commits/index.md +++ b/doc/user/project/repository/signed_commits/index.md @@ -54,13 +54,7 @@ to check a commit's signature. ### Verify commits made in the web UI GitLab signs the commits created using the web UI using SSH. -To verify these commits locally, [follow the steps for SSH](ssh.md#verify-commits-locally) -and add the following public key to the `allowed_signers` file: -`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIADOCCUoN3Q1UPQqUvp845fKy7haJH17qsSkVXzWXilW`. - -```plaintext -noreply@gitlab.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIADOCCUoN3Q1UPQqUvp845fKy7haJH17qsSkVXzWXilW -``` +To verify these commits locally, use the [Web Commits API](../../../../api/web_commits.md#get-public-signing-key) to get the GitLab public key for signing web commits. ## Troubleshooting diff --git a/doc/user/search/exact_code_search.md b/doc/user/search/exact_code_search.md index bffbd273df8..99280226b7d 100644 --- a/doc/user/search/exact_code_search.md +++ b/doc/user/search/exact_code_search.md @@ -43,7 +43,8 @@ For more information, see the history. This feature is available for testing, but not ready for production use. With the Zoekt search API, you can use the [search API](../../api/search.md) for exact code search. -When this feature is disabled, [advanced search](advanced_search.md) or [basic search](index.md) is used instead. +If you want to use [advanced search](advanced_search.md) or basic search instead, see +[specify a search type](index.md#specify-a-search-type). By default, the Zoekt search API is disabled on GitLab.com to avoid breaking changes. To request access to this feature, contact GitLab. diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 8362d2e2051..5fd5376e2ce 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -21,8 +21,18 @@ For code search, GitLab uses these types in this order: or when you search against a non-default branch. This type does not support group or global search. -When exact code search or advanced search is enabled, you can still use -basic search by specifying the `basic_search=true` URL parameter. +## Specify a search type + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161999) in GitLab 17.4. + +To specify a search type, set the `search_type` URL parameter as follows: + +- `search_type=zoekt` for [exact code search](exact_code_search.md) +- `search_type=advanced` for [advanced search](advanced_search.md) +- `search_type=basic` for basic search + +`search_type` replaces the deprecated `basic_search` parameter. +For more information, see [issue 477333](https://gitlab.com/gitlab-org/gitlab/-/issues/477333). ## Global search scopes diff --git a/lib/api/api.rb b/lib/api/api.rb index 43f07b4b19a..e84664c086b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -360,6 +360,7 @@ module API mount ::API::UserCounts mount ::API::UserRunners mount ::API::VirtualRegistries::Packages::Maven + mount ::API::WebCommits mount ::API::Wikis add_open_api_documentation! diff --git a/lib/api/entities/group_association_details.rb b/lib/api/entities/group_association_details.rb new file mode 100644 index 00000000000..766dc012d97 --- /dev/null +++ b/lib/api/entities/group_association_details.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + class GroupAssociationDetails < Entities::BasicGroupDetails + expose :parent_id + expose :organization_id + + expose :access_levels do |group, options| + group.highest_group_member(options[:current_user])&.access_level + end + + expose :visibility, documentation: { type: 'string', example: 'public' } + end + end +end diff --git a/lib/api/entities/personal_access_token_associations.rb b/lib/api/entities/personal_access_token_associations.rb new file mode 100644 index 00000000000..0404b4184b0 --- /dev/null +++ b/lib/api/entities/personal_access_token_associations.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class PersonalAccessTokenAssociations < Grape::Entity + expose :groups, using: Entities::GroupAssociationDetails, documentation: { is_array: true } + expose :projects, using: Entities::ProjectAssociationDetails, documentation: { is_array: true } + end + end +end diff --git a/lib/api/entities/project_association_details.rb b/lib/api/entities/project_association_details.rb new file mode 100644 index 00000000000..f70732d96cb --- /dev/null +++ b/lib/api/entities/project_association_details.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectAssociationDetails < Entities::ProjectIdentity + expose :access_levels do + expose :project_access_level do |project, options| + project.member(options[:current_user])&.access_level + end + + expose :group_access_level do |project, options| + project.group.highest_group_member(options[:current_user])&.access_level if project.group + end + end + + expose :visibility, documentation: { type: 'string', example: 'public' } + expose :web_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab' } + expose :namespace, using: 'API::Entities::NamespaceBasic' + end + end +end diff --git a/lib/api/helpers/protected_branches_helpers.rb b/lib/api/helpers/protected_branches_helpers.rb index 4a968ad1d60..c53c398a84c 100644 --- a/lib/api/helpers/protected_branches_helpers.rb +++ b/lib/api/helpers/protected_branches_helpers.rb @@ -6,6 +6,18 @@ module API extend ActiveSupport::Concern extend Grape::API::Helpers + def authorize_create_protected_branch! + authorize!(:create_protected_branch, user_project) + end + + def authorize_update_protected_branch!(protected_branch) + authorize!(:update_protected_branch, protected_branch) + end + + def authorize_destroy_protected_branch!(protected_branch) + authorize!(:read_protected_branch, protected_branch) + end + params :optional_params_ee do end end diff --git a/lib/api/personal_access_tokens/self_information.rb b/lib/api/personal_access_tokens/self_information.rb index 4f17ca955ac..014399f0e8f 100644 --- a/lib/api/personal_access_tokens/self_information.rb +++ b/lib/api/personal_access_tokens/self_information.rb @@ -4,6 +4,7 @@ module API class PersonalAccessTokens class SelfInformation < ::API::Base include APIGuard + include PaginationParams feature_category :system_access @@ -16,6 +17,20 @@ module API before { authenticate! } + helpers do + def load_groups + finder_params = {} + finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] + GroupsFinder.new(current_user, finder_params).execute + end + + def load_projects + finder_params = {} + finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] + ProjectsFinder.new(current_user: current_user, params: finder_params).execute + end + end + resource :personal_access_tokens do desc "Get single personal access token" do detail 'Get the details of a personal access token by passing it to the API in a header' @@ -30,6 +45,28 @@ module API present access_token, with: Entities::PersonalAccessToken end + desc "Return personal access token associations" do + detail 'Get groups and projects this personal access token can access by passing it to the API in a header' + success code: 200, model: Entities::PersonalAccessToken + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not found' } + ] + tags %w[personal_access_tokens] + end + params do + optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, + desc: 'Limit by minimum access level of authenticated user' + use :pagination + end + get 'self/associations' do + access_token_associations = { + groups: paginate(load_groups), + projects: paginate(load_projects) + } + present access_token_associations, with: Entities::PersonalAccessTokenAssociations, current_user: current_user + end + desc "Revoke a personal access token" do detail 'Revoke a personal access token by passing it to the API in a header' success code: 204 @@ -38,7 +75,6 @@ module API ] tags %w[personal_access_tokens] end - delete 'self' do revoke_token(access_token) end diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 60ce5ddc4f7..bfe496ca1c4 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -88,7 +88,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord post ':id/protected_branches' do - authorize_admin_project + authorize_create_protected_branch! protected_branch = user_project.protected_branches.find_by(name: params[:name]) @@ -127,10 +127,10 @@ module API end # rubocop: disable CodeReuse/ActiveRecord patch ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do - authorize_admin_project - protected_branch = user_project.protected_branches.find_by!(name: params[:name]) + authorize_update_protected_branch!(protected_branch) + declared_params = declared_params(include_missing: false) api_service = ::ProtectedBranches::ApiService.new(user_project, current_user, declared_params) protected_branch = api_service.update(protected_branch) @@ -156,10 +156,10 @@ module API end # rubocop: disable CodeReuse/ActiveRecord delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS, urgency: :low do - authorize_admin_project - protected_branch = user_project.protected_branches.find_by!(name: params[:name]) + authorize_destroy_protected_branch!(protected_branch) + destroy_conditionally!(protected_branch) do destroy_service = ::ProtectedBranches::DestroyService.new(user_project, current_user) destroy_service.execute(protected_branch) diff --git a/lib/api/search.rb b/lib/api/search.rb index ad7f46ccae7..456d6f1f067 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -53,7 +53,6 @@ module API state: params[:state], confidential: params[:confidential], snippets: snippets?, - basic_search: params[:basic_search], num_context_lines: params[:num_context_lines], search_type: params[:search_type], page: params[:page], diff --git a/lib/api/web_commits.rb b/lib/api/web_commits.rb new file mode 100644 index 00000000000..009c0e06a26 --- /dev/null +++ b/lib/api/web_commits.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module API + class WebCommits < ::API::Base + GITALY_PUBLIC_KEY_CACHE_KEY = 'gitaly_public_key' + GITALY_UNAVAILABLE = 'The git server, Gitaly, is not available at this time. Please contact your administrator.' + PUBLIC_KEY_NOT_FOUND = 'Public key not found.' + + feature_category :source_code_management + + before { authenticate_non_get! } + + helpers do + def gitaly_server + @gitaly_server ||= Gitaly::Server.new(::Gitlab::GitalyClient.random_storage) + end + + def server_signature_public_key + gitaly_server.server_signature_public_key + end + + def server_signature_error? + gitaly_server.server_signature_error? + end + + def handle_gitaly_unavailable + render_api_error!(GITALY_UNAVAILABLE, :service_unavailable) + end + + def handle_public_key_not_found + render_api_error!(PUBLIC_KEY_NOT_FOUND, :not_found) + end + + def cache_public_key + Rails.cache.fetch(GITALY_PUBLIC_KEY_CACHE_KEY, expires_in: 1.hour.to_i, skip_nil: true) do + { public_key: server_signature_public_key } + end + end + end + + desc 'Get the public key for web commits' do + detail 'This feature was introduced in GitLab 17.4.' + success code: 200 + failure [ + { code: 503, message: GITALY_UNAVAILABLE }, + { code: 404, message: PUBLIC_KEY_NOT_FOUND } + ] + end + + get 'web_commits/public_key' do + handle_gitaly_unavailable if server_signature_error? + handle_public_key_not_found if server_signature_public_key.empty? + + cache_public_key + end + end +end diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb index 38bb1f649c9..73d01c00cb1 100644 --- a/lib/gitaly/server.rb +++ b/lib/gitaly/server.rb @@ -5,6 +5,8 @@ module Gitaly SHA_VERSION_REGEX = /\A\d+\.\d+\.\d+-\d+-g([a-f0-9]{8})\z/ DEFAULT_REPLICATION_FACTOR = 1 + ServerSignature = Struct.new(:public_key, :error) + class << self def all Gitlab.config.repositories.storages.keys.map { |s| Gitaly::Server.new(s) } @@ -58,6 +60,14 @@ module Gitaly storage_status&.fs_type end + def server_signature_public_key + server_signature&.public_key + end + + def server_signature_error? + !!server_signature.try(:error) + end + def disk_used disk_statistics_storage_status&.used end @@ -99,26 +109,30 @@ module Gitaly Gitlab::GitalyClient.expected_server_version.start_with?(match[1]) end + def server_signature + @server_signature ||= Gitlab::GitalyClient::ServerService.new(@storage).server_signature + rescue GRPC::Unavailable, GRPC::DeadlineExceeded + ServerSignature.new(public_key: nil, error: true) + end + def info - @info ||= - begin - Gitlab::GitalyClient::ServerService.new(@storage).info - rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex - Gitlab::ErrorTracking.track_exception(ex) - # This will show the server as being out of date - Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: []) - end + @info ||= wrapper_gitaly_rpc_errors do + Gitlab::GitalyClient::ServerService.new(@storage).info + end end def disk_statistics - @disk_statistics ||= - begin - Gitlab::GitalyClient::ServerService.new(@storage).disk_statistics - rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex - Gitlab::ErrorTracking.track_exception(ex) - # This will show the server as being out of date - Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: []) - end + @disk_statistics ||= wrapper_gitaly_rpc_errors do + Gitlab::GitalyClient::ServerService.new(@storage).disk_statistics + end + end + + def wrapper_gitaly_rpc_errors + yield + rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex + Gitlab::ErrorTracking.track_exception(ex) + # This will show the server as being out of date + Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: []) end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 09269878559..4d3df2dc0a9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -876,9 +876,9 @@ module Gitlab end # peel_tags slows down the request by a factor of 3-4 - def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX], pointing_at_oids: [], peel_tags: false) + def list_refs(...) wrapped_gitaly_errors do - gitaly_ref_client.list_refs(patterns, pointing_at_oids: pointing_at_oids, peel_tags: peel_tags) + gitaly_ref_client.list_refs(...) end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index e5e4d4b7826..4117a8c59e4 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -167,6 +167,7 @@ module Gitlab { service: 'gitaly.ServerService', method: 'DiskStatistics' }, { service: 'gitaly.ServerService', method: 'ReadinessCheck' }, { service: 'gitaly.ServerService', method: 'ServerInfo' }, + { service: 'gitaly.ServerService', method: 'ServerSignature' }, { service: 'grpc.health.v1.Health', method: 'Check' } ], retryPolicy: { diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 3a92404fc6c..e8348678cff 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -237,7 +237,7 @@ module Gitlab end # peel_tags slows down the request by a factor of 3-4 - def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX], pointing_at_oids: [], peel_tags: false) + def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX], pointing_at_oids: [], peel_tags: false, dynamic_timeout: nil) request = Gitaly::ListRefsRequest.new( repository: @gitaly_repo, patterns: patterns, @@ -245,7 +245,9 @@ module Gitlab peel_tags: peel_tags ) - response = gitaly_client_call(@storage, :ref_service, :list_refs, request, timeout: GitalyClient.fast_timeout) + timeout = dynamic_timeout || GitalyClient.fast_timeout + + response = gitaly_client_call(@storage, :ref_service, :list_refs, request, timeout: timeout) consume_list_refs_response(response) end diff --git a/lib/gitlab/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb index 36bda67c26e..1a9025bd82c 100644 --- a/lib/gitlab/gitaly_client/server_service.rb +++ b/lib/gitlab/gitaly_client/server_service.rb @@ -18,6 +18,10 @@ module Gitlab GitalyClient.call(@storage, :server_service, :disk_statistics, Gitaly::DiskStatisticsRequest.new, timeout: GitalyClient.fast_timeout) end + def server_signature + GitalyClient.call(@storage, :server_service, :server_signature, Gitaly::ServerSignatureRequest.new, timeout: GitalyClient.fast_timeout) + end + def storage_info storage_specific(info) end diff --git a/lib/gitlab/middleware/action_controller_static_context.rb b/lib/gitlab/middleware/action_controller_static_context.rb index 06e542fe5ed..0f3566c3560 100644 --- a/lib/gitlab/middleware/action_controller_static_context.rb +++ b/lib/gitlab/middleware/action_controller_static_context.rb @@ -8,8 +8,6 @@ module Gitlab end def call(env) - return @app.call(env) unless Feature.enabled?(:controller_static_context, Feature.current_request) - req = ActionDispatch::Request.new(env) action_name = req.path_parameters[:action] diff --git a/lib/tasks/gitlab/keep_around.rake b/lib/tasks/gitlab/keep_around.rake new file mode 100644 index 00000000000..5ddf29ddd60 --- /dev/null +++ b/lib/tasks/gitlab/keep_around.rake @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +namespace :gitlab do + namespace :keep_around do + desc "GitLab | Keep-around | Find all orphaned keep-around references for a project" + task orphaned: :gitlab_environment do + warn_user_is_not_gitlab + + project = find_project + + unless project + logger.info Rainbow("Specify the project with PROJECT_ID={number} or PROJECT_PATH={namespace/project-name}").red + exit + end + + create_csv do |csv| + logger.info "Finding keep-around references..." + + refs = project.repository.raw.list_refs( + ["refs/#{::Repository::REF_KEEP_AROUND}/"], + dynamic_timeout: ::Gitlab::GitalyClient.long_timeout + ).each_with_object({}) do |ref, memo| + memo[ref.target] = { + keep_around: ref.name, + count: 0 + } + end + + logger.info "Found #{refs.count} keep-around references" + + add_pipeline_shas(project, refs) + add_merge_request_shas(project, refs) + add_merge_request_diff_shas(project, refs) + add_diff_note_shas(project, refs) + add_note_shas(project, refs) + add_sent_notification_shas(project, refs) + add_todo_shas(project, refs) + + logger.info "Summary:" + logger.info "\tKeep-around references: #{refs.count}" + logger.info "\tPotentially orphaned: #{refs.values.count { |ref| ref[:count] < 1 }}" + + logger.info "Writing CSV..." + refs.each_value do |ref| + csv << [ref[:keep_around], ref[:count]] + end + logger.info "Keep-around orphan report complete" + end + end + + def add_pipeline_shas(project, refs) + logger.info "Checking pipeline shas..." + project.all_pipelines.select(:id, :sha, :before_sha).find_each do |pipeline| + add_match(refs, pipeline.sha) + # before_sha has a project fallback to produce a blank sha. For this + # purpose we would prefer not to load project so we are loading the + # attribute directly. + add_match(refs, pipeline.read_attribute(:before_sha)) + end + end + + def add_merge_request_shas(project, refs) + logger.info "Checking merge request shas..." + merge_requests = MergeRequest.from_and_to_forks(project).select(:id, :merge_commit_sha) + merge_requests.find_each do |merge_request| + add_match(refs, merge_request.merge_commit_sha) + end + end + + def add_merge_request_diff_shas(project, refs) + logger.info "Checking merge request diff shas..." + merge_requests = MergeRequest.from_and_to_forks(project) + merge_request_diffs = MergeRequestDiff + .joins(:merge_request).merge(merge_requests) + .select(:id, :start_commit_sha, :head_commit_sha, :base_commit_sha) + + merge_request_diffs.find_each do |diff| + add_match(refs, diff.start_commit_sha) + add_match(refs, diff.head_commit_sha) + add_match(refs, diff.base_commit_sha) + end + end + + def add_diff_note_shas(project, refs) + logger.info "Checking diff note shas..." + DiffNote.where(project: project).select(:id, :position, :original_position).find_each do |note| + note.shas.each do |sha| + add_match(refs, sha) + end + end + end + + def add_note_shas(project, refs) + logger.info "Checking note shas..." + Note.where(project: project).where.not(commit_id: nil).select(:id, :commit_id).find_each do |note| + add_match(refs, note.commit_id) + end + end + + def add_sent_notification_shas(project, refs) + logger.info "Checking sent notification shas..." + notifications = SentNotification.where(project: project).where.not(commit_id: nil).select(:id, :commit_id) + notifications.find_each do |notification| + add_match(refs, notification.commit_id) + end + end + + def add_todo_shas(project, refs) + logger.info "Checking todo shas..." + Todo.where(project: project).where.not(commit_id: nil).select(:id, :commit_id).find_each do |todo| + add_match(refs, todo.commit_id) + end + end + + def add_match(refs, sha) + return unless refs[sha] + + refs[sha][:count] += 1 + end + + def create_csv + filename = ENV['FILENAME'] + + unless filename + logger.info Rainbow("Specify the CSV output file with FILENAME={path}").red + exit + end + + File.open(filename, "w") do |file| + yield CSV.new(file, headers: %w[keep_around count], write_headers: true) + end + end + + def find_project + if ENV['PROJECT_ID'] + Project.find_by_id(ENV['PROJECT_ID']&.to_i) + elsif ENV['PROJECT_PATH'] + Project.find_by_full_path(ENV['PROJECT_PATH']) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 825ce4a9bfa..abbf27ea67d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24870,6 +24870,9 @@ msgstr "" msgid "GlobalSearch|Fetching aggregations error." msgstr "" +msgid "GlobalSearch|File name match only" +msgstr "" + msgid "GlobalSearch|Files" msgstr "" @@ -28547,6 +28550,9 @@ msgstr "" msgid "Index the instance" msgstr "" +msgid "Indexing CPU to tasks multiplier" +msgstr "" + msgid "Indexing status" msgstr "" @@ -43749,6 +43755,9 @@ msgstr "" msgid "ProtectedBranch|Protect a branch" msgstr "" +msgid "ProtectedBranch|Protected branch was sucessfully created" +msgstr "" + msgid "ProtectedBranch|Protected branches" msgstr "" diff --git a/package.json b/package.json index a24c708d3e6..8cf385ab01a 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-local-rules": "^3.0.2", "eslint-plugin-no-jquery": "2.7.0", - "eslint-plugin-no-unsanitized": "^4.0.2", + "eslint-plugin-no-unsanitized": "^4.1.0", "fake-indexeddb": "^4.0.1", "gettext-extractor": "^3.7.0", "gettext-extractor-vue": "^5.1.0", diff --git a/qa/gems/gitlab-cng/exe/cng b/qa/gems/gitlab-cng/exe/cng index b94a73d1207..6fce4892932 100755 --- a/qa/gems/gitlab-cng/exe/cng +++ b/qa/gems/gitlab-cng/exe/cng @@ -13,6 +13,8 @@ end module Cng def self.run + Gitlab::Cng::Helpers::Output.force_color! if %w[--force-color --force-color=true].any? { |arg| ARGV.include?(arg) } + Gitlab::Cng::CLI.start rescue Gitlab::Cng::CLI::Error => e puts "ERROR: #{e.message}" diff --git a/qa/gems/gitlab-cng/lib/gitlab/cng/commands/_command.rb b/qa/gems/gitlab-cng/lib/gitlab/cng/commands/_command.rb index ab9c521fa93..082b1cba3a2 100644 --- a/qa/gems/gitlab-cng/lib/gitlab/cng/commands/_command.rb +++ b/qa/gems/gitlab-cng/lib/gitlab/cng/commands/_command.rb @@ -8,6 +8,8 @@ module Gitlab class Command < Thor include Helpers::Output + class_option :force_color, type: :boolean + check_unknown_options! private diff --git a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/helpers/output.rb b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/helpers/output.rb index 526740f26c5..fc25e3fee8b 100644 --- a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/helpers/output.rb +++ b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/helpers/output.rb @@ -16,6 +16,22 @@ module Gitlab error: :red }.freeze + class << self + # Global instance of rainbow colorization class + # + # @return [Rainbow] + def rainbow + @rainbow ||= Rainbow.new.tap { |rb| rb.enabled = true if @force_color } + end + + # Force color output + # + # @return [Boolean] + def force_color! + @force_color = true + end + end + private # Print colorized log message to stdout @@ -44,7 +60,7 @@ module Gitlab # @param [Boolean] bright # @return [String] def colorize(message, color, bright: false) - rainbow.wrap(message) + Output.rainbow.wrap(message) .then { |m| bright ? m.bright : m } .then { |m| color ? m.color(color) : m } end @@ -61,13 +77,6 @@ module Gitlab message.gsub(/#{secrets.join('|')}/, "*****") end - - # Instance of rainbow colorization class - # - # @return [Rainbow] - def rainbow - @rainbow ||= Rainbow.new - end end end end diff --git a/qa/gems/gitlab-cng/spec/integration/cng_spec.rb b/qa/gems/gitlab-cng/spec/integration/cng_spec.rb index 7ae94cd7e46..4c3fd3c842a 100644 --- a/qa/gems/gitlab-cng/spec/integration/cng_spec.rb +++ b/qa/gems/gitlab-cng/spec/integration/cng_spec.rb @@ -9,6 +9,7 @@ RSpec.describe "cng" do .and(match(/cng help \[COMMAND\]/)) .and(match(/cng log \[SUBCOMMAND\]/)) .and(match(/cng version/)) + .and(match(/Options:\s+\[--force-color\], \[--no-force-color\], \[--skip-force-color\]/)) ) end end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 8444be86c5f..4b9b56dfaf3 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -11,7 +11,6 @@ RSpec.describe 'Database schema', feature_category: :database do IGNORED_INDEXES_ON_FKS = { ai_testing_terms_acceptances: %w[user_id], # testing terms only have 1 entry, and if the user is deleted the record should remain - application_settings: %w[instance_administration_project_id instance_administrators_group_id], ci_build_trace_metadata: [%w[partition_id build_id], %w[partition_id trace_artifact_id]], # the index on build_id is enough ci_builds: [%w[partition_id stage_id], %w[partition_id execution_config_id], %w[auto_canceled_by_partition_id auto_canceled_by_id], %w[upstream_pipeline_partition_id upstream_pipeline_id], %w[partition_id commit_id]], # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142804#note_1745483081 ci_daily_build_group_report_results: [%w[partition_id last_pipeline_id]], # index on last_pipeline_id is sufficient @@ -145,6 +144,7 @@ RSpec.describe 'Database schema', feature_category: :database do p_catalog_resource_component_usages: %w[used_by_project_id], # No FK constraint because we want to preserve historical usage data p_ci_finished_build_ch_sync_events: %w[build_id], p_ci_finished_pipeline_ch_sync_events: %w[pipeline_id project_namespace_id], + p_ci_job_annotations: %w[partition_id job_id project_id], p_ci_job_artifacts: %w[partition_id project_id job_id], p_ci_pipeline_variables: %w[partition_id], p_ci_builds_execution_configs: %w[partition_id], diff --git a/spec/factories/ci/job_annotations.rb b/spec/factories/ci/job_annotations.rb index 4569b7eea0a..b28dc28460b 100644 --- a/spec/factories/ci/job_annotations.rb +++ b/spec/factories/ci/job_annotations.rb @@ -8,5 +8,7 @@ FactoryBot.define do trait :external_link do data { [{ external_link: { label: 'Example URL', url: 'https://example.com/' } }] } end + + project_id { job.project.id } end end diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index b138a3f6144..f82dea701e4 100644 --- a/spec/features/merge_request/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb @@ -83,11 +83,11 @@ RSpec.describe 'User views diffs', :js, feature_category: :code_review_workflow end it 'toggles container class' do - expect(page).not_to have_css('.content-wrapper > .container-fluid.container-limited') + expect(page).not_to have_css('.content-wrapper > .project-highlight-puc.container-fluid.container-limited') click_link 'Commits' - expect(page).to have_css('.content-wrapper > .container-fluid.container-limited') + expect(page).to have_css('.content-wrapper > .project-highlight-puc.container-fluid.container-limited') end include_examples 'unfold diffs' diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 517bdada675..b90277d2e0c 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -33,53 +33,14 @@ RSpec.describe 'Protected Branches', :js, feature_category: :source_code_managem end context 'logged in as maintainer' do + let(:success_message) { s_('ProtectedBranch|View protected branches as branch rules') } + before do project.add_maintainer(user) sign_in(user) end - it 'allows to create a protected branch with name containing HTML tags' do - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('foobar<\b>') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('foobar<\b>') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('foobar<\b>') - end - - describe 'Delete protected branch' do - before do - create(:protected_branch, project: project, name: 'fix') - expect(ProtectedBranch.count).to eq(1) - end - - it 'removes branch after modal confirmation' do - visit project_branches_path(project) - - find('input[data-testid="branch-search"]').set('fix') - find('input[data-testid="branch-search"]').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - - within_testid('branch-more-actions') do - find('button').click - end - - wait_for_requests - expect(page).to have_button('Delete protected branch', disabled: false) - - find_by_testid('delete-branch-button').click - fill_in 'delete_branch_input', with: 'fix' - click_button 'Yes, delete protected branch' - - expect(page).to have_content('No branches to show') - end - end + it_behaves_like 'setting project protected branches' end context 'logged in as admin' do @@ -88,114 +49,6 @@ RSpec.describe 'Protected Branches', :js, feature_category: :source_code_managem enable_admin_mode!(admin) end - describe "explicit protected branches" do - it "allows creating explicit protected branches" do - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('some->branch') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('some->branch') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('some->branch') - end - - it "shows success alert once protected branch is created" do - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('some->branch') - click_on "Protect" - wait_for_requests - expect(page).to have_content(s_('ProtectedBranch|View protected branches as branch rules')) - end - - it "displays the last commit on the matching branch if it exists" do - commit = create(:commit, project: project) - project.repository.add_branch(admin, 'some-branch', commit.id) - - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('some-branch') - click_on "Protect" - - within(".protected-branches-list") do - expect(page).not_to have_content("matching") - expect(page).not_to have_content("was deleted") - end - end - - it "displays an error message if the named branch does not exist" do - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('some-branch') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('Branch does not exist') } - end - end - - describe "wildcard protected branches" do - it "allows creating protected branches with a wildcard" do - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('*-stable') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('*-stable') - end - - it "displays the number of matching branches", - quarantine: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/flaky-tests/-/issues/3459' do - project.repository.add_branch(admin, 'production-stable', 'master') - project.repository.add_branch(admin, 'staging-stable', 'master') - - visit project_protected_branches_path(project) - - show_add_form - set_defaults - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") do - expect(page).to have_content("2 matching branches") - end - end - - it "displays all the branches matching the wildcard" do - project.repository.add_branch(admin, 'production-stable', 'master') - project.repository.add_branch(admin, 'staging-stable', 'master') - project.repository.add_branch(admin, 'development', 'master') - - visit project_protected_branches_path(project) - - show_add_form - set_protected_branch_name('*-stable') - set_defaults - click_on "Protect" - - visit project_protected_branches_path(project) - click_on "2 matching branches" - - within(".protected-branches-list") do - expect(page).to have_content("production-stable") - expect(page).to have_content("staging-stable") - expect(page).not_to have_content("development") - end - end - end - describe "access control" do before do stub_licensed_features(protected_refs_for_users: false) diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js index 0bee3138607..cb86a704e25 100644 --- a/spec/frontend/ci/catalog/components/list/empty_state_spec.js +++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js @@ -35,6 +35,7 @@ describe('EmptyState', () => { expect(emptyState.props().description).toBe( 'Create a pipeline component repository and make reusing pipeline configurations faster and easier.', ); + expect(emptyState.props().svgPath).toBe('file-mock'); }); }); diff --git a/spec/frontend/lib/utils/regexp_spec.js b/spec/frontend/lib/utils/regexp_spec.js new file mode 100644 index 00000000000..9ec90071d2b --- /dev/null +++ b/spec/frontend/lib/utils/regexp_spec.js @@ -0,0 +1,28 @@ +import { containsPotentialRegex } from '~/lib/utils/regexp'; + +describe('containsPotentialRegex', () => { + it('should return true for a string containing regex elements', () => { + expect(containsPotentialRegex('Does this contain .* a regex?')).toBe(true); + expect(containsPotentialRegex('Special characters like (parentheses) and [brackets].')).toBe( + true, + ); + expect(containsPotentialRegex('Matches \\d digits and \\w word characters.')).toBe(true); + }); + + it('should return true for a string with multiple regex elements', () => { + expect(containsPotentialRegex('Multiple elements: .*\\d+')).toBe(true); + }); + + it('should return false for a string with no regex elements', () => { + expect(containsPotentialRegex('This is a test string.')).toBe(false); + }); + + it('should return false for an empty string', () => { + expect(containsPotentialRegex('')).toBe(false); + }); + + it('should return false for strings with only alphabets and numbers', () => { + expect(containsPotentialRegex('abcdefg12345')).toBe(false); + expect(containsPotentialRegex('simpleTextWithoutRegex')).toBe(false); + }); +}); diff --git a/spec/frontend/projects/settings/components/access_dropdown_spec.js b/spec/frontend/projects/settings/components/access_dropdown_spec.js index bbc87462777..266593907e9 100644 --- a/spec/frontend/projects/settings/components/access_dropdown_spec.js +++ b/spec/frontend/projects/settings/components/access_dropdown_spec.js @@ -50,6 +50,10 @@ jest.mock('~/projects/settings/api/access_dropdown_api', () => ({ })); describe('Access Level Dropdown', () => { + beforeEach(() => { + window.gon = { abilities: { adminProject: true } }; + }); + let wrapper; const defaultToggleClass = '!gl-text-gray-500'; const mockAccessLevelsData = [ @@ -71,9 +75,14 @@ describe('Access Level Dropdown', () => { }, ]; + const abilities = { + adminProject: true, + adminProtectedBranch: false, + }; const createComponent = ({ accessLevelsData = mockAccessLevelsData, accessLevel = ACCESS_LEVELS.PUSH, + glAbilities = abilities, stubs = {}, ...optionalProps } = {}) => { @@ -83,6 +92,9 @@ describe('Access Level Dropdown', () => { accessLevel, ...optionalProps, }, + provide: { + glAbilities, + }, stubs: { GlSprintf, GlDropdown, @@ -121,6 +133,18 @@ describe('Access Level Dropdown', () => { }); }); + describe('withProtectedBranchesAccess', () => { + it('should make an api call for users && groups when user has a license', () => { + createComponent({ + groupsWithProjectAccess: true, + glAbilities: { adminProject: false, adminProtectedBranch: true }, + }); + expect(getUsers).toHaveBeenCalled(); + expect(getGroups).toHaveBeenCalledWith({ withProjectAccess: true }); + expect(getDeployKeys).not.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(); diff --git a/spec/frontend/protected_branches/protected_branch_create_spec.js b/spec/frontend/protected_branches/protected_branch_create_spec.js index e2a0f02e0cf..a7e0dcc6597 100644 --- a/spec/frontend/protected_branches/protected_branch_create_spec.js +++ b/spec/frontend/protected_branches/protected_branch_create_spec.js @@ -17,6 +17,7 @@ describe('ProtectedBranchCreate', () => { window.gon = { merge_access_levels: { roles: [] }, push_access_levels: { roles: [] }, + abilities: { adminProject: true }, }; }); diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js index 463354ab7e1..edcc91e07f2 100644 --- a/spec/frontend/protected_branches/protected_branch_edit_spec.js +++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js @@ -143,6 +143,7 @@ describe('ProtectedBranchEdit', () => { current_project_id: 1, merge_access_levels: { roles: accessLevels }, push_access_levels: { roles: accessLevels }, + abilities: { adminProject: true }, }; jest.spyOn(ProtectedBranchEdit.prototype, 'initToggles').mockImplementation(); diff --git a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap index 60179ac48e3..1d21dc08a5e 100644 --- a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap +++ b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap @@ -24,6 +24,7 @@ exports[`ZoektBlobResults when component loads normally renders component proper > @@ -44,6 +45,7 @@ exports[`ZoektBlobResults when component loads normally renders component proper >
@@ -63,6 +66,7 @@ exports[`ZoektBlobResults when component loads normally renders component proper diff --git a/spec/frontend/search/results/components/blob_body_spec.js b/spec/frontend/search/results/components/blob_body_spec.js index f821c497277..1c209c6b9c9 100644 --- a/spec/frontend/search/results/components/blob_body_spec.js +++ b/spec/frontend/search/results/components/blob_body_spec.js @@ -12,6 +12,7 @@ describe('BlobChunks', () => { wrapper = shallowMountExtended(ZoektBlobResultsChunks, { propsData: { file, + position: 1, }, }); }; @@ -33,6 +34,7 @@ describe('BlobChunks', () => { }, blameLink: 'blame/test.js', fileUrl: 'https://gitlab.com/file/test.js', + position: 1, }); }); diff --git a/spec/frontend/search/results/components/blob_chunks_spec.js b/spec/frontend/search/results/components/blob_chunks_spec.js index d563206d109..44e0cd22fb6 100644 --- a/spec/frontend/search/results/components/blob_chunks_spec.js +++ b/spec/frontend/search/results/components/blob_chunks_spec.js @@ -1,8 +1,14 @@ import { GlIcon, GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BlobChunks from '~/search/results/components/blob_chunks.vue'; +import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import { + EVENT_CLICK_BLOB_RESULT_BLAME_LINE, + EVENT_CLICK_BLOB_RESULT_LINE, +} from '~/search/results/tracking'; describe('BlobChunks', () => { + const { bindInternalEventDocument } = useMockInternalEventsTracking(); let wrapper; const createComponent = (props) => { @@ -22,6 +28,12 @@ describe('BlobChunks', () => { const findLineNumbers = () => wrapper.findAllByTestId('search-blob-line-numbers'); const findLineCode = () => wrapper.findAllByTestId('search-blob-line-code'); const findRootElement = () => wrapper.find('#search-blob-content'); + const findBlameLink = () => + findGlLink().wrappers.filter( + (w) => w.attributes('data-testid') === 'search-blob-line-blame-link', + ); + const findLineLink = () => + findGlLink().wrappers.filter((w) => w.attributes('data-testid') === 'search-blob-line-link'); describe('component basics', () => { beforeEach(() => { @@ -29,13 +41,13 @@ describe('BlobChunks', () => { chunk: { lines: [ { - lineNumber: 1, + lineNumber: '1', richText: '', text: '', __typename: 'SearchBlobLine', }, { - lineNumber: 2, + lineNumber: '2', richText: 'test1', text: 'test1', __typename: 'SearchBlobLine', @@ -47,6 +59,7 @@ describe('BlobChunks', () => { }, blameLink: 'https://gitlab.com/blame/test.js', fileUrl: 'https://gitlab.com/file/test.js', + position: 1, }); }); @@ -73,5 +86,16 @@ describe('BlobChunks', () => { expect(findGlLink().at(1).attributes('title')).toBe('View line in repository'); expect(findGlLink().at(1).text()).toBe('1'); }); + + it.each` + trackedLink | event + ${findBlameLink} | ${EVENT_CLICK_BLOB_RESULT_BLAME_LINE} + ${findLineLink} | ${EVENT_CLICK_BLOB_RESULT_LINE} + `('emits $event on click', ({ trackedLink, event }) => { + const { trackEventSpy } = bindInternalEventDocument(wrapper.element); + trackedLink().at(0).vm.$emit('click'); + + expect(trackEventSpy).toHaveBeenCalledWith(event, { property: '1', value: 1 }, undefined); + }); }); }); diff --git a/spec/frontend/search/results/components/blob_footer_spec.js b/spec/frontend/search/results/components/blob_footer_spec.js index fa671c6b5c9..d3398e86202 100644 --- a/spec/frontend/search/results/components/blob_footer_spec.js +++ b/spec/frontend/search/results/components/blob_footer_spec.js @@ -3,9 +3,12 @@ import { GlSprintf, GlButton, GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BlobFooter from '~/search/results/components/blob_footer.vue'; import eventHub from '~/search/results/event_hub'; +import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import { EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS } from '~/search/results/tracking'; import { mockDataForBlobBody } from '../../mock_data'; describe('BlobFooter', () => { + const { bindInternalEventDocument } = useMockInternalEventsTracking(); let wrapper; let spy; @@ -28,6 +31,7 @@ describe('BlobFooter', () => { beforeEach(() => { createComponent({ file: mockDataForBlobBody, + position: 1, }); spy = jest.spyOn(eventHub, '$emit'); }); @@ -82,6 +86,7 @@ describe('BlobFooter', () => { ], matchCountTotal: 200, }, + position: 1, }); }); @@ -98,5 +103,30 @@ describe('BlobFooter', () => { 'Show less - Too many matches found. Showing 50 chunks out of 200 results. Open the file to view all.', ); }); + + it(`tracks show more or less click`, () => { + const { trackEventSpy } = bindInternalEventDocument(wrapper.element); + findGlButton().vm.$emit('click', { value: 1 }); + + expect(trackEventSpy).toHaveBeenCalledWith( + EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS, + { + label: '1', + property: 'open', + }, + undefined, + ); + + findGlButton().vm.$emit('click', { value: 1 }); + + expect(trackEventSpy).toHaveBeenCalledWith( + EVENT_CLICK_BLOB_RESULTS_SHOW_MORE_LESS, + { + label: '1', + property: 'close', + }, + undefined, + ); + }); }); }); diff --git a/spec/frontend/search/results/components/blob_header_spec.js b/spec/frontend/search/results/components/blob_header_spec.js index e94119228b0..543dedfdb91 100644 --- a/spec/frontend/search/results/components/blob_header_spec.js +++ b/spec/frontend/search/results/components/blob_header_spec.js @@ -1,20 +1,38 @@ +import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports +import Vuex from 'vuex'; +import { GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import BlobHeader from '~/search/results/components/blob_header.vue'; +import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import { EVENT_CLICK_CLIPBOARD_BUTTON, EVENT_CLICK_HEADER_LINK } from '~/search/results/tracking'; +import { MOCK_QUERY } from '../../mock_data'; + +Vue.use(Vuex); describe('BlobHeader', () => { + const { bindInternalEventDocument } = useMockInternalEventsTracking(); let wrapper; const createComponent = (props) => { + const store = new Vuex.Store({ + state: { + query: MOCK_QUERY, + }, + }); + wrapper = shallowMountExtended(BlobHeader, { propsData: { ...props, }, + store, }); }; const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findHeaderLink = () => wrapper.findComponent(GlLink); const findFileIcon = () => wrapper.findComponent(FileIcon); const findProjectPath = () => wrapper.findByTestId('project-path-content'); const findProjectName = () => wrapper.findByTestId('file-name-content'); @@ -51,4 +69,25 @@ describe('BlobHeader', () => { expect(findProjectName().exists()).toBe(true); }); }); + + describe('events', () => { + beforeEach(() => { + createComponent({ + filePath: 'test/file.js', + projectPath: 'Testjs/Test', + fileUrl: 'https://gitlab.com/test/file.js', + }); + }); + + it.each` + trackedLink | event + ${findHeaderLink} | ${EVENT_CLICK_HEADER_LINK} + ${findClipboardButton} | ${EVENT_CLICK_CLIPBOARD_BUTTON} + `('emits $event on click', ({ trackedLink, event }) => { + const { trackEventSpy } = bindInternalEventDocument(wrapper.element); + trackedLink().vm.$emit('click'); + + expect(trackEventSpy).toHaveBeenCalledWith(event, {}, undefined); + }); + }); }); diff --git a/spec/frontend/search/results/components/zoekt_blob_results_spec.js b/spec/frontend/search/results/components/zoekt_blob_results_spec.js index 812e5af3d16..eb3792937fe 100644 --- a/spec/frontend/search/results/components/zoekt_blob_results_spec.js +++ b/spec/frontend/search/results/components/zoekt_blob_results_spec.js @@ -5,8 +5,8 @@ import { GlLoadingIcon, GlCard } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue'; import waitForPromises from 'helpers/wait_for_promises'; - import EmptyResult from '~/search/results/components/result_empty.vue'; + import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data'; jest.mock('~/alert'); diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js index 26b33bcd46d..97378263c7e 100644 --- a/spec/frontend/user_lists/components/user_lists_table_spec.js +++ b/spec/frontend/user_lists/components/user_lists_table_spec.js @@ -6,6 +6,7 @@ import { timeagoLanguageCode } from '~/lib/utils/datetime/timeago_utility'; import UserListsTable from '~/user_lists/components/user_lists_table.vue'; import { userList } from 'jest/feature_flags/mock_data'; import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat'; +import { newDate } from '~/lib/utils/datetime/date_calculation_utility'; jest.mock('timeago.js', () => ({ format: jest.fn().mockReturnValue('2 weeks ago'), @@ -29,7 +30,7 @@ describe('User Lists Table', () => { userList.user_xids.replace(/,/g, ', '), ); expect(wrapper.find('[data-testid="ffUserListTimestamp"]').text()).toBe('created 2 weeks ago'); - expect(timeago.format).toHaveBeenCalledWith(userList.created_at, timeagoLanguageCode); + expect(timeago.format).toHaveBeenCalledWith(newDate(userList.created_at), timeagoLanguageCode); }); it('should set the title for a tooltip on the created stamp', () => { diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb index 83df4b28757..d9bb0fed09a 100644 --- a/spec/lib/gitaly/server_spec.rb +++ b/spec/lib/gitaly/server_spec.rb @@ -136,6 +136,38 @@ RSpec.describe Gitaly::Server do end end + describe "#server_signature_public_key" do + context 'when the server signature returns a public key' do + let(:public_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFcykDaUT7x4oXyUCfgqJhfAXRbhtsLl4fi4142zrPCI' } + + before do + allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive_message_chain(:server_signature, :public_key).and_return(public_key) + end + end + + it 'returns a public key and no errors' do + expect(server.server_signature_public_key).to eq(public_key) + expect(server.server_signature_error?).to be(false) + end + end + end + + describe "#server_signature_error?" do + context 'when the server signature raises a GRPC error' do + before do + allow_next_instance_of(::Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive(:server_signature).and_raise(GRPC::Unavailable) + end + end + + it 'returns an error and no public_key' do + expect(server.server_signature_public_key).to be_nil + expect(server.server_signature_error?).to eq(true) + end + end + end + describe 'replication_factor' do context 'when examining for a given server' do let(:storage_status) { double('storage_status', storage_name: 'default') } diff --git a/spec/lib/gitlab/gitaly_client/server_service_spec.rb b/spec/lib/gitlab/gitaly_client/server_service_spec.rb new file mode 100644 index 00000000000..3c9bf14c6bb --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/server_service_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GitalyClient::ServerService, feature_category: :gitaly do + let_it_be(:project) { create(:project, :repository) } + let(:storage_name) { project.repository_storage } + let(:client) { described_class.new(storage_name) } + + describe '#server_signature' do + it 'sends a server_signature message' do + # rubocop:disable RSpec/AnyInstanceOf -- expect_next_instance_of does not work here + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163430#note_2086684781 + expect_any_instance_of(Gitaly::ServerService::Stub).to receive(:server_signature).and_return([]) + # rubocop:enable RSpec/AnyInstanceOf + + client.server_signature + end + end +end diff --git a/spec/lib/gitlab/middleware/action_controller_static_context_spec.rb b/spec/lib/gitlab/middleware/action_controller_static_context_spec.rb index 5109c8108e2..2cafbaf1dd1 100644 --- a/spec/lib/gitlab/middleware/action_controller_static_context_spec.rb +++ b/spec/lib/gitlab/middleware/action_controller_static_context_spec.rb @@ -12,26 +12,12 @@ RSpec.describe Gitlab::Middleware::ActionControllerStaticContext, feature_catego let(:app) { ->(_env) { [200, {}, ["Hello World"]] } } - context 'when feature flag is disabled' do - it 'does not update context' do - stub_feature_flags(controller_static_context: false) + it 'populates context with static controller attributes' do + described_class.new(app).call(env) - described_class.new(app).call(env) - - expect(Gitlab::ApplicationContext.current.keys).not_to include('meta.feature_category', 'meta.caller_id') - end - end - - context 'when feature flag is enabled' do - it 'populates context with static controller attributes' do - stub_feature_flags(controller_static_context: true) - - described_class.new(app).call(env) - - expect(Labkit::Context.current.to_h).to include({ - 'meta.feature_category' => 'groups_and_projects', - 'meta.caller_id' => 'Dashboard::GroupsController#index' - }) - end + expect(Labkit::Context.current.to_h).to include({ + 'meta.feature_category' => 'groups_and_projects', + 'meta.caller_id' => 'Dashboard::GroupsController#index' + }) end end diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb index aa18b687791..f4a18a42e67 100644 --- a/spec/lib/marginalia_spec.rb +++ b/spec/lib/marginalia_spec.rb @@ -53,8 +53,6 @@ RSpec.describe 'Marginalia spec' do end it 'generates a query that includes the component and value' do - stub_feature_flags(controller_static_context: false) - component_map.each do |component, value| expect(recorded.log.last).to include("#{component}:#{value}") end @@ -76,8 +74,6 @@ RSpec.describe 'Marginalia spec' do end it 'generates a query that includes the component and value' do - stub_feature_flags(controller_static_context: false) - component_map.each do |component, value| expect(recorded.log.last).to include("#{component}:#{value}") end @@ -151,6 +147,7 @@ RSpec.describe 'Marginalia spec' do def make_request(correlation_id, action_name) request_env = Rack::MockRequest.env_for('/') + ::Labkit::Context.push(caller_id: MarginaliaTestController.endpoint_id_for_action(action_name)) ::Labkit::Correlation::CorrelationId.use_id(correlation_id) do MarginaliaTestController.action(action_name).call(request_env) end diff --git a/spec/migrations/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records_spec.rb b/spec/migrations/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records_spec.rb new file mode 100644 index 00000000000..073647f5594 --- /dev/null +++ b/spec/migrations/db/post_migrate/20240826081110_backfill_null_project_ci_job_annotation_records_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillNullProjectCiJobAnnotationRecords, + migration: :gitlab_ci, + feature_category: :job_artifacts, + migration_version: 20240826081110 do + let(:connection) { Ci::ApplicationRecord.connection } + let(:builds_table) { table(:p_ci_builds, database: :ci, primary_key: :id) } + let(:annotations_table) { table(:p_ci_job_annotations, database: :ci, primary_key: :id) } + + let(:build1) { builds_table.create!(name: "build", project_id: 1, partition_id: 100) } + let(:build2) { builds_table.create!(name: "build", project_id: 2, partition_id: 100) } + + let!(:annotations1) do + annotations_table.create!( + name: "annotations1", + job_id: build1.id, + partition_id: 100, + project_id: -1) + end + + let!(:annotations2) do + annotations_table.create!( + name: "annotations2", + job_id: build2.id, + partition_id: 100, + project_id: -1) + end + + describe '#up' do + it 'backfills when annotations without project' do + expect { migrate! } + .to change { annotations1.reload.project_id }.to(build1.project_id) + .and change { annotations2.reload.project_id }.to(build2.project_id) + end + end +end diff --git a/spec/models/anti_abuse/reports/note_spec.rb b/spec/models/anti_abuse/reports/note_spec.rb index 1b39a6bb4f7..d8a4192f235 100644 --- a/spec/models/anti_abuse/reports/note_spec.rb +++ b/spec/models/anti_abuse/reports/note_spec.rb @@ -6,7 +6,7 @@ RSpec.describe AntiAbuse::Reports::Note, feature_category: :insider_threat do describe 'Concerns' do let_it_be(:factory) { :abuse_report_note } let_it_be(:discussion_factory) { :abuse_report_discussion_note } - let_it_be(:note1) { create(:abuse_report_note) } + let_it_be(:note1) { create(:abuse_report_note, note: 'some note') } let_it_be(:note2) { create(:abuse_report_note) } let_it_be(:reply) { create(:abuse_report_note, in_reply_to: note1) } @@ -26,5 +26,11 @@ RSpec.describe AntiAbuse::Reports::Note, feature_category: :insider_threat do describe 'Validations' do it { is_expected.to validate_presence_of(:abuse_report) } end + + describe 'Callbacks' do + it 'caches the html field' do + expect(note1.note_html).to include('some note

') + end + end end end diff --git a/spec/models/ci/job_annotation_spec.rb b/spec/models/ci/job_annotation_spec.rb index 46cd8b087fe..4562465c210 100644 --- a/spec/models/ci/job_annotation_spec.rb +++ b/spec/models/ci/job_annotation_spec.rb @@ -6,11 +6,12 @@ RSpec.describe Ci::JobAnnotation, feature_category: :job_artifacts do let_it_be_with_refind(:job) { create(:ci_build, :success) } describe 'validations' do - subject { create(:ci_job_annotation, job: job) } + let!(:annotations) { create(:ci_job_annotation, job: job) } it { is_expected.to belong_to(:job).class_name('Ci::Build').inverse_of(:job_annotations) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(255) } + it { is_expected.to validate_presence_of(:project_id) } end describe '.create' do @@ -66,15 +67,5 @@ RSpec.describe Ci::JobAnnotation, feature_category: :job_artifacts do end end end - - context 'without job' do - let(:annotation) { build(:ci_job_annotation, job: nil) } - - it { is_expected.to validate_presence_of(:partition_id) } - - it 'does not change the partition_id value' do - expect { annotation.valid? }.not_to change { annotation.partition_id } - end - end end end diff --git a/spec/models/packages/conan/metadatum_spec.rb b/spec/models/packages/conan/metadatum_spec.rb index ae916ced391..f0a3ef318ff 100644 --- a/spec/models/packages/conan/metadatum_spec.rb +++ b/spec/models/packages/conan/metadatum_spec.rb @@ -11,8 +11,6 @@ RSpec.describe Packages::Conan::Metadatum, type: :model, feature_category: :pack describe 'validations' do let(:fifty_one_characters) { 'f_a' * 17 } - let(:thirty_three_characters) { 'a' * 33 } - let(:seventeen_characters) { 'a' * 17 } it { is_expected.to validate_presence_of(:package) } it { is_expected.to validate_presence_of(:package_username) } @@ -49,41 +47,6 @@ RSpec.describe Packages::Conan::Metadatum, type: :model, feature_category: :pack it { is_expected.not_to allow_value("my@channel").for(:package_channel) } end - describe '#architecture' do - it { is_expected.to validate_length_of(:architecture).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:architecture) } - end - - describe '#os' do - it { is_expected.to validate_length_of(:os).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:os) } - end - - describe '#build_type' do - it { is_expected.to validate_length_of(:build_type).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:build_type) } - end - - describe '#compiler' do - it { is_expected.to validate_length_of(:compiler).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:compiler) } - end - - describe '#compiler_libcxx' do - it { is_expected.to validate_length_of(:compiler_libcxx).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:compiler_libcxx) } - end - - describe '#compiler_cppstd' do - it { is_expected.to validate_length_of(:compiler_cppstd).is_at_most(32) } - it { is_expected.not_to allow_value(thirty_three_characters).for(:compiler_cppstd) } - end - - describe '#compiler_version' do - it { is_expected.to validate_length_of(:compiler_version).is_at_most(16) } - it { is_expected.not_to allow_value(seventeen_characters).for(:compiler_version) } - end - describe '#username_channel_none_values' do let_it_be(:package) { create(:conan_package) } diff --git a/spec/requests/api/personal_access_tokens/self_information_spec.rb b/spec/requests/api/personal_access_tokens/self_information_spec.rb index 3cfaaaf7d3f..91fb9a3801d 100644 --- a/spec/requests/api/personal_access_tokens/self_information_spec.rb +++ b/spec/requests/api/personal_access_tokens/self_information_spec.rb @@ -99,4 +99,51 @@ RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :sy end end end + + describe 'GET /personal_access_tokens/self/associations' do + let(:path) { '/personal_access_tokens/self/associations' } + + context 'when token is invalid' do + it 'returns 401' do + get api(path, personal_access_token: instance_double(PersonalAccessToken, token: 'invalidtoken')) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when token is valid' do + context 'when token has no associations' do + it 'returns empty arrays', :aggregate_failures do + get api(path, personal_access_token: token) + + expect(json_response).to eq({ "groups" => [], "projects" => [] }) + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'when token has associations' do + before do + group = create(:group, :private, name: "test_group", developers: current_user) + sub_group = create(:group, :private, name: "test_subgroup", developers: current_user, parent: group) + create(:project, :private, name: "test_project", maintainers: current_user, group: sub_group) + end + + it 'returns associations', :aggregate_failures do + get api(path, personal_access_token: token) + + expected_group_names = json_response["groups"].pluck("name") + expect(expected_group_names).to match_array(%w[test_group test_subgroup]) + expect(response).to have_gitlab_http_status(:success) + end + + it 'filters associations by min_access_level', :aggregate_failures do + get api("#{path}?min_access_level=40", personal_access_token: token) + + expect(json_response["groups"]).to be_empty + expect(json_response["projects"][0]["name"]).to eq("test_project") + expect(response).to have_gitlab_http_status(:success) + end + end + end + end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index e21eb374be3..fbbc732ac24 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -766,9 +766,9 @@ RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: context 'when requesting basic search' do it 'passes the parameter to search service' do - expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')) + expect(SearchService).to receive(:new).with(user, hash_including(search_type: 'basic')) - get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' } + get api(endpoint, user), params: { scope: 'issues', search: 'awesome', search_type: 'basic' } end end diff --git a/spec/requests/api/web_commits_spec.rb b/spec/requests/api/web_commits_spec.rb new file mode 100644 index 00000000000..0391abbf998 --- /dev/null +++ b/spec/requests/api/web_commits_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::WebCommits, feature_category: :source_code_management do + describe 'GET /web_commits/public_key' do + context 'when Gitaly is available' do + let(:public_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFcykDaUT7x4oXyUCfgqJhfAXRbhtsLl4fi4142zrPCI' } + + before do + allow_next_instance_of(::Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive_message_chain(:server_signature, :public_key).and_return(public_key) + end + end + + context 'and the public key is not found' do + let(:public_key) { '' } + + it 'returns not found' do + get api('/web_commits/public_key') + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Public key not found.') + end + + it 'does not cache the public key' do + expect(Rails.cache).not_to receive(:fetch).with( + described_class::GITALY_PUBLIC_KEY_CACHE_KEY, expires_in: 1.hour.to_i, skip_nil: true + ).and_call_original + + get api('/web_commits/public_key') + end + end + + context 'and the public key is found' do + it 'returns the public key' do + get api('/web_commits/public_key') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['public_key']).to eq(public_key) + end + + it 'caches the public key' do + expect(Rails.cache).to receive(:fetch).with( + described_class::GITALY_PUBLIC_KEY_CACHE_KEY, expires_in: 1.hour.to_i, skip_nil: true + ).and_call_original + + get api('/web_commits/public_key') + end + end + end + + context 'when Gitaly is unavailable' do + before do + allow_next_instance_of(::Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive(:server_signature).and_raise(GRPC::Unavailable) + end + end + + it 'does not cache the public key' do + expect(Rails.cache).not_to receive(:fetch).with( + described_class::GITALY_PUBLIC_KEY_CACHE_KEY, expires_in: 1.hour.to_i, skip_nil: true + ).and_call_original + + get api('/web_commits/public_key') + end + + it 'returns service unavailable' do + get api('/web_commits/public_key') + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['message']) + .to eq('The git server, Gitaly, is not available at this time. Please contact your administrator.') + end + end + end +end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 0bf77d82db2..dbde700e5bc 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -193,103 +193,60 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared 'meta.caller_id' => 'Dashboard::GroupsController#index' } end - context 'when feature flag is disabled' do - before do - stub_feature_flags(controller_static_context: false) - end - - context 'and action is successfully called' do - it 'pushes static context to current context' do - controller = nil - allow_next_instance_of(Dashboard::GroupsController) do |instance| - controller = instance - end - - get '/dashboard/groups' # randomly picked route - - expect(response).to have_gitlab_http_status(:found) - expect(controller.instance_variable_get(:@current_context)).to include expected_context + context 'when action is successfully called' do + it 'pushes static context to current context' do + context = {} + allow_next_instance_of(Dashboard::GroupsController) do |_controller| + context.merge!(Gitlab::ApplicationContext.current.to_h) end - end - context 'and an exception is thrown before action' do - it 'does not pushes static context to current context before controller callbacks' do - context = {} - unexpected_error = 'boom 💣💥' + get '/dashboard/groups' # randomly picked route - allow_next_instance_of(Dashboard::GroupsController) do |controller| - # picking up a random before_action method to raise an "unexpected" exception - allow(controller).to receive(:authenticate_user!).and_raise(unexpected_error) - - context.merge!(Gitlab::ApplicationContext.current.to_h) - end - - expect { get '/dashboard/groups' }.to raise_error(unexpected_error) - expect(context.keys).not_to include(['meta.feature_category', 'meta.caller_id']) - end + expect(response).to have_gitlab_http_status(:found) + expect(context).to include expected_context end end - context 'when feature flag is enabled' do - before do - stub_feature_flags(controller_static_context: true) - end + context 'when an exception is thrown before action' do + it 'pushes static context to current context before controller callbacks' do + context = {} + unexpected_error = 'boom 💣💥' - context 'when action is successfully called' do - it 'pushes static context to current context' do - context = {} - allow_next_instance_of(Dashboard::GroupsController) do |_controller| - context.merge!(Gitlab::ApplicationContext.current.to_h) - end + allow_next_instance_of(Dashboard::GroupsController) do |controller| + # picking up a random before_action method to raise an "unexpected" exception + allow(controller).to receive(:authenticate_user!).and_raise(unexpected_error) - get '/dashboard/groups' # randomly picked route - - expect(response).to have_gitlab_http_status(:found) - expect(context).to include expected_context + context.merge!(Gitlab::ApplicationContext.current.to_h) end + + expect { get '/dashboard/groups' }.to raise_error(unexpected_error) + expect(context).to include expected_context end + end - context 'when an exception is thrown before action' do - it 'pushes static context to current context before controller callbacks' do - context = {} - unexpected_error = 'boom 💣💥' + context 'when controller overrides feature_category with nil' do + it 'ignores nil feature category override' do + context = {} - allow_next_instance_of(Dashboard::GroupsController) do |controller| - # picking up a random before_action method to raise an "unexpected" exception - allow(controller).to receive(:authenticate_user!).and_raise(unexpected_error) + allow_next_instance_of(Projects::NotesController) do |controller| + # mimicking a bug overriding feature_category with nil + allow(controller).to receive(:feature_category).and_return(nil) - context.merge!(Gitlab::ApplicationContext.current.to_h) - end - - expect { get '/dashboard/groups' }.to raise_error(unexpected_error) - expect(context).to include expected_context + context.merge!(Gitlab::ApplicationContext.current.to_h) end - end - context 'when controller overrides feature_category with nil' do - it 'ignores nil feature category override' do - context = {} + project = create(:project, :public) + project_snippet = create(:project_snippet, project: project) + create(:note_on_project_snippet, project: project, noteable: project_snippet) + # picking a route targeting a controller that overrides feature_category + get project_noteable_notes_path( + project, + target_type: 'project_snippet', + target_id: project_snippet.id, + html: true + ) - allow_next_instance_of(Projects::NotesController) do |controller| - # mimicking a bug overriding feature_category with nil - allow(controller).to receive(:feature_category).and_return(nil) - - context.merge!(Gitlab::ApplicationContext.current.to_h) - end - - project = create(:project, :public) - project_snippet = create(:project_snippet, project: project) - create(:note_on_project_snippet, project: project, noteable: project_snippet) - # picking a route targeting a controller that overrides feature_category - get project_noteable_notes_path( - project, - target_type: 'project_snippet', - target_id: project_snippet.id, - html: true - ) - - expect(context).to include({ 'meta.feature_category' => 'team_planning' }) - end + expect(context).to include({ 'meta.feature_category' => 'team_planning' }) end end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 983582f38da..686a70de5da 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -42,6 +42,8 @@ Capybara.register_driver :chrome do |app| # Chrome won't work properly in a Docker container in sandbox mode options.add_argument("no-sandbox") + options.add_argument("disable-search-engine-choice-screen") + # Run headless by default unless WEBDRIVER_HEADLESS specified options.add_argument("headless") unless ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i || ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 8032c78c755..d211b1ce921 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -140,7 +140,7 @@ module GraphqlHelpers arg_style: :internal_prepared, # Args are in internal format, but should use more rigorous processing query: nil # Query to evaluate the field ) - field = to_base_field(field, object_type) + field = to_base_field(field, object_type).ensure_loaded ctx[:current_user] = current_user unless current_user == :not_given query ||= GraphQL::Query.new(schema, context: ctx.to_h) extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead) diff --git a/spec/support/shared_examples/projects/protected_branches_shared_examples.rb b/spec/support/shared_examples/projects/protected_branches_shared_examples.rb new file mode 100644 index 00000000000..e6ed34026d2 --- /dev/null +++ b/spec/support/shared_examples/projects/protected_branches_shared_examples.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'setting project protected branches' do + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('some->branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('some->branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some->branch') + end + + it "shows success alert once protected branch is created" do + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('some->branch') + click_on "Protect" + wait_for_requests + expect(page).to have_content(success_message) + end + + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(admin, 'some-branch', commit.id) + + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") do + expect(page).not_to have_content("matching") + expect(page).not_to have_content("was deleted") + end + end + + it "displays an error message if the named branch does not exist" do + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('some-unexisting-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('Branch does not exist') } + end + end + + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end + + it "displays the number of matching branches", + quarantine: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/flaky-tests/-/issues/3459' do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + + visit project_protected_branches_path(project) + + show_add_form + set_defaults + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") do + expect(page).to have_content("2 matching branches") + end + end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + project.repository.add_branch(admin, 'development', 'master') + + visit project_protected_branches_path(project) + + show_add_form + set_protected_branch_name('*-stable') + set_defaults + click_on "Protect" + + visit project_protected_branches_path(project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end +end diff --git a/spec/tasks/gitlab/keep_around_rake_spec.rb b/spec/tasks/gitlab/keep_around_rake_spec.rb new file mode 100644 index 00000000000..3ad20f0b68c --- /dev/null +++ b/spec/tasks/gitlab/keep_around_rake_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop:disable RSpec/AvoidTestProf -- this is not a migration spec +RSpec.describe 'keep-around tasks', :silence_stdout, feature_category: :source_code_management do + include ProjectForksHelper + + before do + Rake.application.rake_require 'tasks/gitlab/keep_around' + end + + describe 'orphaned' do + subject { run_rake_task('gitlab:keep_around:orphaned') } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:keep_around_shas) do + # Keep-around references only on branch tips is not necessarily accurate, + # but this constant gives convenient access to commit IDs that actually + # exist. + ::TestEnv::BRANCH_SHA.slice( + 'master', + 'changes-with-whitespace', + 'changes-with-only-whitespace' + ).values.uniq + end + + let(:logger) { instance_double(::Logger) } + let(:file) { Tempfile.new("orphan_report.csv") } + let(:project_id_env) { project.id } + let(:project_path_env) { nil } + let(:filename_env) { file.path } + + before do + allow(main_object).to receive(:logger).and_return(logger).at_least(:once) + + allow(logger).to receive(:info).at_least(:once) + allow(logger).to receive(:debug).at_least(:once) + + stub_env('PROJECT_ID', project_id_env) + stub_env('PROJECT_PATH', project_path_env) + stub_env('FILENAME', filename_env) + + ::Gitlab::Git::KeepAround.new(project.repository).execute( + keep_around_shas, + source: "keep_around_rake_spec" + ) + end + + after do + file.unlink + end + + shared_examples 'orphans found' do |keep_around_count:, orphan_count:| + it 'prints a summary' do + expect(logger).to receive(:info).with("Summary:") + expect(logger).to receive(:info).with("\tKeep-around references: #{keep_around_count}") + expect(logger).to receive(:info).with("\tPotentially orphaned: #{orphan_count}") + + run_rake_task('gitlab:keep_around:orphaned') + end + end + + context "without project" do + let(:project_id_env) { nil } + + it 'exits with instructions' do + expect(logger).to receive(:info).with( + "Specify the project with PROJECT_ID={number} or PROJECT_PATH={namespace/project-name}" + ) + + expect do + run_rake_task('gitlab:keep_around:orphaned') + end.to raise_error(SystemExit) + end + end + + context "without filename" do + let(:filename_env) { nil } + + it 'exits with instructions' do + expect(logger).to receive(:info).with("Specify the CSV output file with FILENAME={path}") + + expect do + run_rake_task('gitlab:keep_around:orphaned') + end.to raise_error(SystemExit) + end + end + + context "with project path" do + let(:project_id_env) { nil } + let(:project_path_env) { project.full_path } + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 3 + end + + context "with only orphaned keep-arounds" do + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 3 + end + + context "for pipeline keep-arounds" do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + + context "for merge request keep-arounds" do + let_it_be(:fork) { fork_project(project, nil, repository: true) } + let_it_be(:merge_request) { create(:merge_request, target_project: project, source_project: fork) } + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + + context "for diff note keep-arounds" do + let_it_be(:diff_note) do + create(:diff_note_on_merge_request, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) + end + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + + context "for note keep-arounds" do + let_it_be(:note) { create(:note_on_commit, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) } + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + + context "for sent notification keep-arounds" do + let_it_be(:sent_notification) do + create(:sent_notification, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) + end + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + + context "for todo keep-arounds" do + let_it_be(:todo) { create(:todo, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) } + + it_behaves_like 'orphans found', + keep_around_count: 3, + orphan_count: 2 + end + end +end +# rubocop:enable RSpec/AvoidTestProf diff --git a/yarn.lock b/yarn.lock index 7ad717dac1f..d5d8d115f5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6670,10 +6670,10 @@ eslint-plugin-no-jquery@2.7.0: resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72" integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w== -eslint-plugin-no-unsanitized@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.2.tgz#e872b302cdfb5fe1262db989ba29cfcc334b499b" - integrity sha512-Pry0S9YmHoz8NCEMRQh7N0Yexh2MYCNPIlrV52hTmS7qXnTghWsjXouF08bgsrrZqaW9tt1ZiK3j5NEmPE+EjQ== +eslint-plugin-no-unsanitized@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.0.tgz#2c914e8ea8048c3afaac8f0c12384747aba6497a" + integrity sha512-9A8Yrbkkex8e56ivxJ2f5dXN2Js2BmKC8QgmeYZjadyiGUngo3KLXDlq6ZzalmCHyLwLF5MoQLPR6FWlNc+Qbw== eslint-plugin-promise@^7.0.0: version "7.0.0"