diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-14 21:12:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-14 21:12:06 +0300 |
commit | 30b01621d3e9e83f9f2d8a94dba6888eba7e8cc1 (patch) | |
tree | 309eb7898d9d73ab1d1c0cd65e43757a7201edfe | |
parent | b119503b7039d1e79b87300a145afdcd1145c2d6 (diff) |
Add latest changes from gitlab-org/gitlab@master
50 files changed, 707 insertions, 353 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index af1cf88b176..7dd676e6a47 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -13,7 +13,6 @@ /doc/.vale/ @marcel.amirault @eread @aqualls @cnorris /doc/administration/geo/ @axil /doc/administration/gitaly/ @eread -/doc/administration/integration/ @aqualls /doc/administration/lfs/ @aqualls /doc/administration/monitoring/ @ngaskill /doc/administration/operations/ @axil @eread @marcia @@ -36,7 +35,6 @@ /doc/development/value_stream_analytics.md @msedlakjakubowski /doc/gitlab-basics/ @aqualls /doc/install/ @axil -/doc/integration/ @aqualls @eread /doc/operations/ @ngaskill @axil /doc/push_rules/ @aqualls /doc/ssh/ @eread @@ -65,8 +63,6 @@ /doc/user/project/ @aqualls @axil @eread @msedlakjakubowski @ngaskill /doc/user/project/clusters/ @marcia /doc/user/project/import/ @ngaskill @msedlakjakubowski -/doc/user/project/integrations/ @aqualls -/doc/user/project/integrations/prometheus_library/ @ngaskill /doc/user/project/issues/ @msedlakjakubowski /doc/user/project/merge_requests/ @aqualls @eread /doc/user/project/milestones/ @msedlakjakubowski @@ -142,6 +138,12 @@ /doc/user/project/settings/import_export.md @aqualls /doc/user/snippets.md @aqualls +[Docs Ecosystem] +/doc/administration/integration/ @kpaizee +/doc/integration/ @kpaizee +/doc/user/project/integrations/ @kpaizee +/doc/user/project/integrations/prometheus_library/ @ngaskill + [Docs Growth] /doc/administration/instance_review.md @kpaizee /doc/api/invitations.md @kpaizee diff --git a/Gemfile.lock b/Gemfile.lock index 50f396a5743..dcc34698053 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -900,7 +900,7 @@ GEM orm_adapter (0.5.0) os (1.1.1) parallel (1.20.1) - parser (3.0.0.0) + parser (3.0.2.0) ast (~> 2.4.1) parslet (1.8.2) pastel (0.8.0) diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue index 158b5f45d1c..bce13751448 100644 --- a/app/assets/javascripts/batch_comments/components/review_bar.vue +++ b/app/assets/javascripts/batch_comments/components/review_bar.vue @@ -1,5 +1,6 @@ <script> import { mapActions, mapGetters } from 'vuex'; +import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants'; import PreviewDropdown from './preview_dropdown.vue'; import PublishButton from './publish_button.vue'; @@ -18,6 +19,12 @@ export default { } }, }, + mounted() { + document.body.classList.add(REVIEW_BAR_VISIBLE_CLASS_NAME); + }, + beforeDestroy() { + document.body.classList.remove(REVIEW_BAR_VISIBLE_CLASS_NAME); + }, methods: { ...mapActions('batchComments', ['expandAllDiscussions']), }, diff --git a/app/assets/javascripts/batch_comments/constants.js b/app/assets/javascripts/batch_comments/constants.js index b309c339fc8..5e026251e0b 100644 --- a/app/assets/javascripts/batch_comments/constants.js +++ b/app/assets/javascripts/batch_comments/constants.js @@ -1,3 +1,5 @@ export const CHANGES_TAB = 'diffs'; export const DISCUSSION_TAB = 'notes'; export const SHOW_TAB = 'show'; + +export const REVIEW_BAR_VISIBLE_CLASS_NAME = 'review-bar-visible'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 57162c46002..a2ea42e963c 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -229,7 +229,6 @@ export default { 'isBatchLoading', 'isBatchLoadingError', ]), - ...mapGetters('batchComments', ['draftsCount']), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -664,7 +663,6 @@ export default { <div v-if="renderFileTree" :style="{ width: `${treeWidth}px` }" - :class="{ 'review-bar-visible': draftsCount > 0 }" class="diff-tree-list js-diff-tree-list px-3 pr-md-0" > <panel-resizer diff --git a/app/assets/javascripts/lib/logger/hello.js b/app/assets/javascripts/lib/logger/hello.js new file mode 100644 index 00000000000..18fa35ab55b --- /dev/null +++ b/app/assets/javascripts/lib/logger/hello.js @@ -0,0 +1,16 @@ +const HANDSHAKE = String.fromCodePoint(0x1f91d); +const MAG = String.fromCodePoint(0x1f50e); + +export const logHello = () => { + // eslint-disable-next-line no-console + console.log( + `%cWelcome to GitLab!%c + +Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute! + +${HANDSHAKE} Contribute to GitLab: https://about.gitlab.com/community/contribute/ +${MAG} Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new`, + `padding-top: 0.5em; font-size: 2em;`, + 'padding-bottom: 0.5em;', + ); +}; diff --git a/app/assets/javascripts/lib/logger/hello_deferred.js b/app/assets/javascripts/lib/logger/hello_deferred.js new file mode 100644 index 00000000000..ce1dd91cb37 --- /dev/null +++ b/app/assets/javascripts/lib/logger/hello_deferred.js @@ -0,0 +1,5 @@ +export const logHelloDeferred = async () => { + const { logHello } = await import(/* webpackChunkName: 'hello' */ './hello'); + + logHello(); +}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 15c483485f1..b96a2607552 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -19,6 +19,7 @@ import initAlertHandler from './alert_handler'; import { removeFlashClickListener } from './flash'; import initTodoToggle from './header'; import initLayoutNav from './layout_nav'; +import { logHelloDeferred } from './lib/logger/hello_deferred'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime/timeago_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; @@ -40,6 +41,8 @@ import { initHeaderSearchApp } from '~/header_search'; import 'ee_else_ce/main_ee'; import 'jh_else_ce/main_jh'; +logHelloDeferred(); + applyGitLabUIConfig(); // expose jQuery as global (TODO: remove these) diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index ebe0138f046..6a282df99bf 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -10,6 +10,7 @@ import { REPORT_TYPE_CONTAINER_SCANNING, REPORT_TYPE_CLUSTER_IMAGE_SCANNING, REPORT_TYPE_COVERAGE_FUZZING, + REPORT_TYPE_CORPUS_MANAGEMENT, REPORT_TYPE_API_FUZZING, REPORT_TYPE_LICENSE_COMPLIANCE, } from '~/vue_shared/security_reports/constants'; @@ -104,6 +105,12 @@ export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath( { anchor: 'configuration' }, ); +export const CORPUS_MANAGEMENT_NAME = __('Corpus Management'); +export const CORPUS_MANAGEMENT_DESCRIPTION = s__( + 'SecurityConfiguration|Manage corpus files used as mutation sources in coverage fuzzing.', +); +export const CORPUS_MANAGEMENT_CONFIG_TEXT = s__('SecurityConfiguration|Manage corpus'); + export const API_FUZZING_NAME = __('API Fuzzing'); export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.'); export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index'); @@ -202,6 +209,14 @@ export const securityFeatures = [ helpPath: COVERAGE_FUZZING_HELP_PATH, configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH, type: REPORT_TYPE_COVERAGE_FUZZING, + secondary: gon?.features?.corpusManagement + ? { + type: REPORT_TYPE_CORPUS_MANAGEMENT, + name: CORPUS_MANAGEMENT_NAME, + description: CORPUS_MANAGEMENT_DESCRIPTION, + configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT, + } + : {}, }, ]; diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index 4a50dfbd82f..b024e92bd0e 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -24,6 +24,7 @@ export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning'; export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning'; export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning'; export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; +export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management'; export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4e05775fca6..94912b1c641 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -122,7 +122,9 @@ .right-sidebar { position: fixed; top: $header-height; - bottom: 0; + // Default value for CSS var must contain a unit + // stylelint-disable-next-line length-zero-no-unit + bottom: var(--review-bar-height, 0px); right: 0; transition: width $sidebar-transition-duration; background: $gray-light; diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 157c454183a..a62d47071d4 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -15,6 +15,10 @@ class BranchesFinder < GitRefsFinder end end + def total + repository.branch_count + end + private def names diff --git a/app/finders/repositories/tree_finder.rb b/app/finders/repositories/tree_finder.rb new file mode 100644 index 00000000000..2ea5a8856ec --- /dev/null +++ b/app/finders/repositories/tree_finder.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Repositories + class TreeFinder < GitRefsFinder + attr_reader :user_project + + CommitMissingError = Class.new(StandardError) + + def initialize(user_project, params = {}) + super(user_project.repository, params) + + @user_project = user_project + end + + def execute(gitaly_pagination: false) + raise CommitMissingError unless commit_exists? + + request_params = { recursive: recursive } + request_params[:pagination_params] = pagination_params if gitaly_pagination + tree = user_project.repository.tree(commit.id, path, **request_params) + + tree.sorted_entries + end + + def total + # This is inefficient and we'll look at replacing this implementation + Gitlab::Cache.fetch_once([user_project, repository.commit, :tree_size, commit.id, path, recursive]) do + user_project.repository.tree(commit.id, path, recursive: recursive).entries.size + end + end + + def commit_exists? + commit.present? + end + + private + + def commit + @commit ||= user_project.commit(ref) + end + + def ref + params[:ref] || user_project.default_branch + end + + def path + params[:path] + end + + def recursive + params[:recursive] + end + + def pagination_params + { + limit: params[:per_page] || Kaminari.config.default_per_page, + page_token: params[:page_token] + } + end + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 719b1215c90..ebb4ba39dd6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -468,85 +468,28 @@ class ApplicationSetting < ApplicationRecord length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') }, allow_blank: true - validates :throttle_unauthenticated_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_packages_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_packages_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_files_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_unauthenticated_files_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_git_lfs_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_git_lfs_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_web_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_web_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_packages_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_packages_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_files_api_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_authenticated_files_api_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_protected_paths_requests_per_period, - presence: true, - numericality: { only_integer: true, greater_than: 0 } - - validates :throttle_protected_paths_period_in_seconds, - presence: true, - numericality: { only_integer: true, greater_than: 0 } + with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do + validates :throttle_unauthenticated_api_requests_per_period + validates :throttle_unauthenticated_api_period_in_seconds + validates :throttle_unauthenticated_requests_per_period + validates :throttle_unauthenticated_period_in_seconds + validates :throttle_unauthenticated_packages_api_requests_per_period + validates :throttle_unauthenticated_packages_api_period_in_seconds + validates :throttle_unauthenticated_files_api_requests_per_period + validates :throttle_unauthenticated_files_api_period_in_seconds + validates :throttle_authenticated_api_requests_per_period + validates :throttle_authenticated_api_period_in_seconds + validates :throttle_authenticated_git_lfs_requests_per_period + validates :throttle_authenticated_git_lfs_period_in_seconds + validates :throttle_authenticated_web_requests_per_period + validates :throttle_authenticated_web_period_in_seconds + validates :throttle_authenticated_packages_api_requests_per_period + validates :throttle_authenticated_packages_api_period_in_seconds + validates :throttle_authenticated_files_api_requests_per_period + validates :throttle_authenticated_files_api_period_in_seconds + validates :throttle_protected_paths_requests_per_period + validates :throttle_protected_paths_period_in_seconds + end validates :notes_create_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } diff --git a/app/services/ci/stuck_builds/drop_helpers.rb b/app/services/ci/stuck_builds/drop_helpers.rb new file mode 100644 index 00000000000..f79b805c23d --- /dev/null +++ b/app/services/ci/stuck_builds/drop_helpers.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Ci + module StuckBuilds + module DropHelpers + def drop(builds, failure_reason:) + fetch(builds) do |build| + drop_build :outdated, build, failure_reason + end + end + + def drop_stuck(builds, failure_reason:) + fetch(builds) do |build| + break unless build.stuck? + + drop_build :stuck, build, failure_reason + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def fetch(builds) + loop do + jobs = builds.includes(:tags, :runner, project: [:namespace, :route]) + .limit(100) + .to_a + + break if jobs.empty? + + jobs.each do |job| + Gitlab::ApplicationContext.with_context(project: job.project) { yield(job) } + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def drop_build(type, build, reason) + Gitlab::AppLogger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{build.status}, failure_reason: #{reason})" + Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'stuck_ci_jobs_worker_drop_build') do |b| + b.drop(reason) + end + rescue StandardError => ex + build.doom! + + track_exception_for_build(ex, build) + end + + def track_exception_for_build(ex, build) + Gitlab::ErrorTracking.track_exception(ex, + build_id: build.id, + build_name: build.name, + build_stage: build.stage, + pipeline_id: build.pipeline_id, + project_id: build.project_id + ) + end + end + end +end diff --git a/app/services/ci/stuck_builds/drop_service.rb b/app/services/ci/stuck_builds/drop_service.rb index fd7293e6ab9..3fee9a94381 100644 --- a/app/services/ci/stuck_builds/drop_service.rb +++ b/app/services/ci/stuck_builds/drop_service.rb @@ -3,6 +3,8 @@ module Ci module StuckBuilds class DropService + include DropHelpers + BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour BUILD_PENDING_OUTDATED_TIMEOUT = 1.day BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour @@ -55,57 +57,6 @@ module Ci BUILD_RUNNING_OUTDATED_TIMEOUT.ago ) end - - def drop(builds, failure_reason:) - fetch(builds) do |build| - drop_build :outdated, build, failure_reason - end - end - - def drop_stuck(builds, failure_reason:) - fetch(builds) do |build| - break unless build.stuck? - - drop_build :stuck, build, failure_reason - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def fetch(builds) - loop do - jobs = builds.includes(:tags, :runner, project: [:namespace, :route]) - .limit(100) - .to_a - - break if jobs.empty? - - jobs.each do |job| - Gitlab::ApplicationContext.with_context(project: job.project) { yield(job) } - end - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def drop_build(type, build, reason) - Gitlab::AppLogger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{build.status}, failure_reason: #{reason})" - Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'stuck_ci_jobs_worker_drop_build') do |b| - b.drop(reason) - end - rescue StandardError => ex - build.doom! - - track_exception_for_build(ex, build) - end - - def track_exception_for_build(ex, build) - Gitlab::ErrorTracking.track_exception(ex, - build_id: build.id, - build_name: build.name, - build_stage: build.stage, - pipeline_id: build.pipeline_id, - project_id: build.project_id - ) - end end end end diff --git a/config/feature_flags/development/npm_finder_query_avoid_duplicated_conditions.yml b/config/feature_flags/development/repository_tree_gitaly_pagination.yml index 4313e64e93e..afae937b62e 100644 --- a/config/feature_flags/development/npm_finder_query_avoid_duplicated_conditions.yml +++ b/config/feature_flags/development/repository_tree_gitaly_pagination.yml @@ -1,8 +1,8 @@ --- -name: npm_finder_query_avoid_duplicated_conditions -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69572 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340099 +name: repository_tree_gitaly_pagination +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67509 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340419 milestone: '14.3' type: development -group: group::package +group: group::source code default_enabled: false diff --git a/data/deprecations/templates/_deprecation_template.md.erb b/data/deprecations/templates/_deprecation_template.md.erb index 1a77689080e..b6f532147fd 100644 --- a/data/deprecations/templates/_deprecation_template.md.erb +++ b/data/deprecations/templates/_deprecation_template.md.erb @@ -15,7 +15,7 @@ Do not edit this page directly. To add a deprecation, use the example.yml file in `/data/deprecations/templates` as a template, then run `bin/rake gitlab:docs:compile_deprecations`. --> -<% if milestones.any? %> +<% if milestones.any? -%> <%- milestones.each do |milestone| %> ## <%= milestone %> <%- deprecations.select{|d| d["removal_milestone"] == milestone}.each do |deprecation| %> diff --git a/doc/ci/examples/index.md b/doc/ci/examples/index.md index bee480917e5..2c2c6ecd30f 100644 --- a/doc/ci/examples/index.md +++ b/doc/ci/examples/index.md @@ -121,6 +121,7 @@ For examples of setting up GitLab CI/CD for cloud-based environments, see: - [How to autoscale continuous deployment with GitLab Runner on DigitalOcean](https://about.gitlab.com/blog/2018/06/19/autoscale-continuous-deployment-gitlab-runner-digital-ocean/) - [How to create a CI/CD pipeline with Auto Deploy to Kubernetes using GitLab and Helm](https://about.gitlab.com/blog/2017/09/21/how-to-create-ci-cd-pipeline-with-autodeploy-to-kubernetes-using-gitlab-and-helm/) - Video: [Demo - Deploying from GitLab to OpenShift Container Cluster](https://youtu.be/EwbhA53Jpp4) +- Tutorial: [Set up a GitLab.com Civo Kubernetes integration with GitPod](https://gitlab.com/k33g_org/k33g_org.gitlab.io/-/issues/82) See also the following video overviews: diff --git a/doc/development/database/efficient_in_operator_queries.md b/doc/development/database/efficient_in_operator_queries.md index 207fa6c3832..bc72bce30bf 100644 --- a/doc/development/database/efficient_in_operator_queries.md +++ b/doc/development/database/efficient_in_operator_queries.md @@ -463,7 +463,7 @@ Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new( <details> <summary>Expand this sentence to see the SQL query.</summary> -<pre><code lang='sql'> +<pre><code> SELECT "issues".* FROM (WITH RECURSIVE "array_cte" AS MATERIALIZED @@ -582,6 +582,7 @@ FROM WHERE (COUNT <> 0)) issues LIMIT 20 </code> +</pre> </details> NOTE: @@ -613,7 +614,7 @@ end #### Keyset pagination The optimization works out of the box with GraphQL and the `keyset_paginate` helper method. -Read more about [keyset pagination](database/keyset_pagination.md). +Read more about [keyset pagination](keyset_pagination.md). ```ruby array_scope = Group.find(9970).all_projects.select(:id) @@ -637,7 +638,7 @@ issues = Issue #### Offset pagination with Kaminari The `ActiveRecord` scope produced by the `InOperatorOptimization` class can be used in -[offset-paginated](database/pagination_guidelines.md#offset-pagination) +[offset-paginated](pagination_guidelines.md#offset-pagination) queries. ```ruby diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 12f0c61a3fb..a1c2bcdbdd7 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1081,10 +1081,15 @@ To select your avatar: ## Images Images, including screenshots, can help a reader better understand a concept. -However, they can be hard to maintain, and should be used sparingly. +However, they should be used sparingly because: -Before including an image in the documentation, ensure it provides value to the -reader. +- They tend to become out-of-date. +- They are difficult and expensive to localize. +- They cannot be read by screen readers. + +If you do include an image in the documentation, ensure it provides value. +Don't use `lorem ipsum` text. Try to replicate how the feature would be +used in a real-world scenario, and [use realistic text](#fake-user-information). ### Capture the image diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index b718fd26d45..08d67b9614f 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -160,14 +160,14 @@ Do not use **Developer permissions**. A user who is assigned the Developer role See [the Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/d/disable-disabled) for guidance on **disable**. Use **inactive** or **off** instead. ([Vale](../testing.md#vale) rule: [`InclusionAbleism.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionAbleism.yml)) -## dropdown, dropdown list +## dropdown list -Do not use. Use **list** instead. +Use **dropdown list** to refer to the UI element. Do not use **dropdown** without **list** after it. +Do not use **drop-down** (hyphenated), **dropdown menu**, or other variants. -Include the descriptor when writing about lists. Start with the list name, -then follow with the item the user should select. For example: +For example: -- From the **Visibility** list, select **Public**. +- From the **Visibility** dropdown list, select **Public**. ## earlier @@ -331,11 +331,8 @@ Use **later** when talking about version numbers. ## list -Use instead of **dropdown**, **drop-down** or **dropdown list**. You select an item from a list. For example: - -- From the **Availability** list, select **public**. - -The list name, and the items you select, should be bold. +Do not use **list** when referring to a [**dropdown list**](#dropdown-list). +Use the full phrase **dropdown list** instead. ## log in, log on diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md index a4c2ea043cc..9ce012366ef 100644 --- a/doc/user/admin_area/analytics/dev_ops_report.md +++ b/doc/user/admin_area/analytics/dev_ops_report.md @@ -45,6 +45,7 @@ feature is available. > - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2. > - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2. > - Multi-select [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2. +> - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3. DevOps Adoption shows you which groups in your organization are using the most essential features of GitLab: diff --git a/doc/user/clusters/management_project_template.md b/doc/user/clusters/management_project_template.md index b00fa873b4b..e10d45264f9 100644 --- a/doc/user/clusters/management_project_template.md +++ b/doc/user/clusters/management_project_template.md @@ -62,7 +62,7 @@ the pipeline runs, Helmfile tries to either install or update your apps accordin cluster and Helm releases. If you change this attribute to `installed: false`, Helmfile tries try to uninstall this app from your cluster. [Read more](https://github.com/roboll/helmfile) about how Helmfile works. -Furthermore, each app has an `applications/{app}/values.yaml` file. This is the +Furthermore, each app has an `applications/{app}/values.yaml` file (`applicaton/{app}/values.yaml.gotmpl` in case of GitLab Runner). This is the place where you can define some default values for your app's Helm chart. Some apps already have defaults pre-defined by GitLab. diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md index 5c84a343da9..554d01039ad 100644 --- a/doc/user/group/devops_adoption/index.md +++ b/doc/user/group/devops_adoption/index.md @@ -13,6 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - Fuzz Testing metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/330398) in GitLab 14.2. > - Dependency Scanning metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328034) in GitLab 14.2. > - Multiselect [added](https://gitlab.com/gitlab-org/gitlab/-/issues/333586) in GitLab 14.2. +> - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3. Prerequisites: diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb index 4a7827c1d25..34e126c73fc 100644 --- a/lib/api/helpers/packages/npm.rb +++ b/lib/api/helpers/packages/npm.rb @@ -60,7 +60,7 @@ module API finder = ::Packages::Npm::PackageFinder.new( package_name, namespace: namespace, - last_of_each_version: Feature.disabled?(:npm_finder_query_avoid_duplicated_conditions) + last_of_each_version: false ) finder.last&.project_id diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb index 7ff4439ce04..dbfc0a61577 100644 --- a/lib/api/npm_project_packages.rb +++ b/lib/api/npm_project_packages.rb @@ -48,14 +48,13 @@ module API put ':package_name', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do authorize_create_package!(project) - track_package_event('push_package', :npm, category: 'API::NpmPackages', project: project, user: current_user, namespace: project.namespace) - created_package = ::Packages::Npm::CreatePackageService .new(project, current_user, params.merge(build: current_authenticated_job)).execute if created_package[:status] == :error render_api_error!(created_package[:message], created_package[:http_status]) else + track_package_event('push_package', :npm, category: 'API::NpmPackages', project: project, user: current_user, namespace: project.namespace) created_package end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 20320d1b7ae..3c9255e3117 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -51,18 +51,22 @@ module API optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :path, type: String, desc: 'The path of the tree' optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' + use :pagination + optional :pagination, type: String, values: %w(legacy keyset), default: 'legacy', desc: 'Specify the pagination method' + + given pagination: -> (value) { value == 'keyset' } do + optional :page_token, type: String, desc: 'Record from which to start the keyset pagination' + end end get ':id/repository/tree' do - ref = params[:ref] || user_project.default_branch - path = params[:path] || nil + tree_finder = ::Repositories::TreeFinder.new(user_project, declared_params(include_missing: false)) + + not_found!("Tree") unless tree_finder.commit_exists? - commit = user_project.commit(ref) - not_found!('Tree') unless commit + tree = Gitlab::Pagination::GitalyKeysetPager.new(self, user_project).paginate(tree_finder) - tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) - entries = ::Kaminari.paginate_array(tree.sorted_entries) - present paginate(entries), with: Entities::TreeObject + present tree, with: Entities::TreeObject end desc 'Get raw blob contents from the repository' diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb index 50a8f189df0..e30e526f017 100644 --- a/lib/gitlab/cycle_analytics/summary/base.rb +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -16,6 +16,10 @@ module Gitlab def value raise NotImplementedError, "Expected #{self.name} to implement value" end + + private + + attr_reader :project, :options end end end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb index ea16226a865..403cec5ed19 100644 --- a/lib/gitlab/cycle_analytics/summary/deploy.rb +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -24,3 +24,5 @@ module Gitlab end end end + +Gitlab::CycleAnalytics::Summary::Deploy.prepend_mod_with('Gitlab::CycleAnalytics::Summary::Deploy') diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb index 0fb8faae8ce..9b00b323301 100644 --- a/lib/gitlab/database/load_balancing/load_balancer.rb +++ b/lib/gitlab/database/load_balancing/load_balancer.rb @@ -245,7 +245,7 @@ module Gitlab end def request_cache - base = RequestStore[:gitlab_load_balancer] ||= {} + base = SafeRequestStore[:gitlab_load_balancer] ||= {} base[self] ||= {} end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index ddf04bd8e79..75588ad980c 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -127,6 +127,7 @@ module Gitlab entries = response.flat_map do |message| cursor = message.pagination_cursor if message.pagination_cursor + message.entries.map do |gitaly_tree_entry| Gitlab::Git::Tree.new( id: gitaly_tree_entry.oid, diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb index b05891066ac..a16bf7a379c 100644 --- a/lib/gitlab/pagination/gitaly_keyset_pager.rb +++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb @@ -14,23 +14,39 @@ module Gitlab # It is expected that the given finder will respond to `execute` method with `gitaly_pagination: true` option # and supports pagination via gitaly. def paginate(finder) - return paginate_via_gitaly(finder) if keyset_pagination_enabled? - return paginate_first_page_via_gitaly(finder) if paginate_first_page? + return paginate_via_gitaly(finder) if keyset_pagination_enabled?(finder) + return paginate_first_page_via_gitaly(finder) if paginate_first_page?(finder) - branches = ::Kaminari.paginate_array(finder.execute) + records = ::Kaminari.paginate_array(finder.execute) Gitlab::Pagination::OffsetPagination .new(request_context) - .paginate(branches) + .paginate(records) end private - def keyset_pagination_enabled? - Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) && params[:pagination] == 'keyset' + def keyset_pagination_enabled?(finder) + return false unless params[:pagination] == "keyset" + + if finder.is_a?(BranchesFinder) + Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) + elsif finder.is_a?(::Repositories::TreeFinder) + Feature.enabled?(:repository_tree_gitaly_pagination, project, default_enabled: :yaml) + else + false + end end - def paginate_first_page? - Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) && (params[:page].blank? || params[:page].to_i == 1) + def paginate_first_page?(finder) + return false unless params[:page].blank? || params[:page].to_i == 1 + + if finder.is_a?(BranchesFinder) + Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) + elsif finder.is_a?(::Repositories::TreeFinder) + Feature.enabled?(:repository_tree_gitaly_pagination, project, default_enabled: :yaml) + else + false + end end def paginate_via_gitaly(finder) @@ -43,7 +59,7 @@ module Gitlab # Headers are added to immitate offset pagination, while it is the default option def paginate_first_page_via_gitaly(finder) finder.execute(gitaly_pagination: true).tap do |records| - total = project.repository.branch_count + total = finder.total per_page = params[:per_page].presence || Kaminari.config.default_per_page Gitlab::Pagination::OffsetHeaderBuilder.new( diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4f4e2d4122e..b99b4c66722 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9240,6 +9240,9 @@ msgstr "" msgid "Copy value" msgstr "" +msgid "Corpus Management" +msgstr "" + msgid "Corpus Management|Are you sure you want to delete the corpus?" msgstr "" @@ -29782,6 +29785,12 @@ msgstr "" msgid "SecurityConfiguration|Immediately begin risk analysis and remediation with application security features. Start with SAST and Secret Detection, available to all plans. Upgrade to Ultimate to get all features, including:" msgstr "" +msgid "SecurityConfiguration|Manage corpus" +msgstr "" + +msgid "SecurityConfiguration|Manage corpus files used as mutation sources in coverage fuzzing." +msgstr "" + msgid "SecurityConfiguration|Manage profiles for use by DAST scans." msgstr "" diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f3500301e22..1108c606df3 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -534,6 +534,14 @@ FactoryBot.define do end end + trait :coverage_fuzzing do + options do + { + artifacts: { reports: { coverage_fuzzing: 'gl-coverage-fuzzing-report.json' } } + } + end + end + trait :license_scanning do options do { diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index a62dd3842db..f9d525c33a4 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -259,4 +259,11 @@ RSpec.describe BranchesFinder do end end end + + describe '#total' do + subject { branch_finder.total } + + it { is_expected.to be_an(Integer) } + it { is_expected.to eq(repository.branch_count) } + end end diff --git a/spec/finders/repositories/tree_finder_spec.rb b/spec/finders/repositories/tree_finder_spec.rb new file mode 100644 index 00000000000..0d70d5f92d3 --- /dev/null +++ b/spec/finders/repositories/tree_finder_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Repositories::TreeFinder do + include RepoHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, creator: user) } + + let(:repository) { project.repository } + let(:tree_finder) { described_class.new(project, params) } + let(:params) { {} } + let(:first_page_ids) { tree_finder.execute.map(&:id) } + let(:second_page_token) { first_page_ids.last } + + describe "#execute" do + subject { tree_finder.execute(gitaly_pagination: true) } + + it "returns an array" do + is_expected.to be_an(Array) + end + + it "includes 20 items by default" do + expect(subject.size).to eq(20) + end + + it "accepts a gitaly_pagination argument" do + expect(repository).to receive(:tree).with(anything, anything, recursive: nil, pagination_params: { limit: 20, page_token: nil }).and_call_original + expect(tree_finder.execute(gitaly_pagination: true)).to be_an(Array) + + expect(repository).to receive(:tree).with(anything, anything, recursive: nil).and_call_original + expect(tree_finder.execute(gitaly_pagination: false)).to be_an(Array) + end + + context "commit doesn't exist" do + let(:params) do + { ref: "nonesuchref" } + end + + it "raises an error" do + expect { subject }.to raise_error(described_class::CommitMissingError) + end + end + + describe "pagination_params" do + let(:params) do + { per_page: 5, page_token: nil } + end + + it "has the per_page number of items" do + expect(subject.size).to eq(5) + end + + it "doesn't include any of the first page records" do + first_page_ids = subject.map(&:id) + second_page = described_class.new(project, { per_page: 5, page_token: first_page_ids.last }).execute(gitaly_pagination: true) + + expect(second_page.map(&:id)).not_to include(*first_page_ids) + end + end + end + + describe "#total", :use_clean_rails_memory_store_caching do + subject { tree_finder.total } + + it { is_expected.to be_an(Integer) } + + it "only calculates the total once" do + expect(repository).to receive(:tree).once.and_call_original + + 2.times { tree_finder.total } + end + end + + describe "#commit_exists?" do + subject { tree_finder.commit_exists? } + + context "ref exists" do + let(:params) do + { ref: project.default_branch } + end + + it { is_expected.to be(true) } + end + + context "ref is missing" do + let(:params) do + { ref: "nonesuchref" } + end + + it { is_expected.to be(false) } + end + end +end diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js new file mode 100644 index 00000000000..f50db6ab210 --- /dev/null +++ b/spec/frontend/batch_comments/components/review_bar_spec.js @@ -0,0 +1,42 @@ +import { shallowMount } from '@vue/test-utils'; +import ReviewBar from '~/batch_comments/components/review_bar.vue'; +import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '~/batch_comments/constants'; +import createStore from '../create_batch_comments_store'; + +describe('Batch comments review bar component', () => { + let store; + let wrapper; + + const createComponent = (propsData = {}) => { + store = createStore(); + + wrapper = shallowMount(ReviewBar, { + store, + propsData, + }); + }; + + beforeEach(() => { + document.body.className = ''; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('it adds review-bar-visible class to body when review bar is mounted', async () => { + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + + createComponent(); + + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); + }); + + it('it removes review-bar-visible class to body when review bar is destroyed', async () => { + createComponent(); + + wrapper.destroy(); + + expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); + }); +}); diff --git a/spec/frontend/batch_comments/create_batch_comments_store.js b/spec/frontend/batch_comments/create_batch_comments_store.js new file mode 100644 index 00000000000..10dc6fe196e --- /dev/null +++ b/spec/frontend/batch_comments/create_batch_comments_store.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import batchCommentsModule from '~/batch_comments/stores/modules/batch_comments'; +import notesModule from '~/notes/stores/modules'; + +Vue.use(Vuex); + +export default function createDiffsStore() { + return new Vuex.Store({ + modules: { + notes: notesModule(), + batchComments: batchCommentsModule(), + }, + }); +} diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 1a9e182ee2b..9dc82bbdc93 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -705,24 +705,4 @@ describe('diffs/components/app', () => { ); }); }); - - describe('diff file tree is aware of review bar', () => { - it('it does not have review-bar-visible class when review bar is not visible', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; - }); - - expect(wrapper.find('.js-diff-tree-list').exists()).toBe(true); - expect(wrapper.find('.js-diff-tree-list.review-bar-visible').exists()).toBe(false); - }); - - it('it does have review-bar-visible class when review bar is visible', () => { - createComponent({}, ({ state }) => { - state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; - state.batchComments.drafts = ['draft message']; - }); - - expect(wrapper.find('.js-diff-tree-list.review-bar-visible').exists()).toBe(true); - }); - }); }); diff --git a/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap new file mode 100644 index 00000000000..791ec05befd --- /dev/null +++ b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`~/lib/logger/hello logHello console logs a friendly hello message 1`] = ` +Array [ + Array [ + "%cWelcome to GitLab!%c + +Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute! + +🤝 Contribute to GitLab: https://about.gitlab.com/community/contribute/ +🔎 Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new", + "padding-top: 0.5em; font-size: 2em;", + "padding-bottom: 0.5em;", + ], +] +`; diff --git a/spec/frontend/lib/logger/hello_deferred_spec.js b/spec/frontend/lib/logger/hello_deferred_spec.js new file mode 100644 index 00000000000..3233cbff0dc --- /dev/null +++ b/spec/frontend/lib/logger/hello_deferred_spec.js @@ -0,0 +1,17 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { logHello } from '~/lib/logger/hello'; +import { logHelloDeferred } from '~/lib/logger/hello_deferred'; + +jest.mock('~/lib/logger/hello'); + +describe('~/lib/logger/hello_deferred', () => { + it('dynamically imports and calls logHello', async () => { + logHelloDeferred(); + + expect(logHello).not.toHaveBeenCalled(); + + await waitForPromises(); + + expect(logHello).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/lib/logger/hello_spec.js b/spec/frontend/lib/logger/hello_spec.js new file mode 100644 index 00000000000..39abe0e0dd0 --- /dev/null +++ b/spec/frontend/lib/logger/hello_spec.js @@ -0,0 +1,20 @@ +import { logHello } from '~/lib/logger/hello'; + +describe('~/lib/logger/hello', () => { + let consoleLogSpy; + + beforeEach(() => { + // We don't `mockImplementation` so we can validate there's no errors thrown + consoleLogSpy = jest.spyOn(console, 'log'); + }); + + describe('logHello', () => { + it('console logs a friendly hello message', () => { + expect(consoleLogSpy).not.toHaveBeenCalled(); + + logHello(); + + expect(consoleLogSpy.mock.calls).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index b9e0132badb..8053f5261c0 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::CycleAnalytics::StageSummary do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let(:options) { { from: 1.day.ago } } let(:args) { { options: options, current_user: user } } let(:user) { create(:user, :admin) } @@ -62,6 +63,8 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do end describe "#commits" do + let!(:project) { create(:project, :repository) } + subject { stage_summary.second } context 'when from date is given' do @@ -132,115 +135,5 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do end end - describe "#deploys" do - subject { stage_summary.third } - - context 'when from date is given' do - before do - Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } - Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } - end - - it "finds the number of deploys made created after the 'from date'" do - expect(subject[:value]).to eq('1') - end - - it 'returns the localized title' do - Gitlab::I18n.with_locale(:ru) do - expect(subject[:title]).to eq(n_('Deploy', 'Deploys', 1)) - end - end - end - - it "doesn't find commits from other projects" do - Timecop.freeze(5.days.from_now) do - create(:deployment, :success, project: create(:project, :repository)) - end - - expect(subject[:value]).to eq('-') - end - - context 'when `to` parameter is given' do - before do - Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } - Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } - end - - it "doesn't find any record" do - options[:to] = Time.now - - expect(subject[:value]).to eq('-') - end - - it "finds records created between `from` and `to` range" do - options[:from] = 10.days.ago - options[:to] = 10.days.from_now - - expect(subject[:value]).to eq('2') - end - end - end - - describe '#deployment_frequency' do - subject { stage_summary.fourth[:value] } - - it 'includes the unit: `per day`' do - expect(stage_summary.fourth[:unit]).to eq _('per day') - end - - before do - Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } - end - - it 'returns 0.0 when there were deploys but the frequency was too low' do - options[:from] = 30.days.ago - - # 1 deployment over 30 days - # frequency of 0.03, rounded off to 0.0 - expect(subject).to eq('0') - end - - it 'returns `-` when there were no deploys' do - options[:from] = 4.days.ago - - # 0 deployment in the last 4 days - expect(subject).to eq('-') - end - - context 'when `to` is nil' do - it 'includes range until now' do - options[:from] = 6.days.ago - options[:to] = nil - - # 1 deployment over 7 days - expect(subject).to eq('0.1') - end - end - - context 'when `to` is given' do - before do - Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project, finished_at: Time.zone.now) } - end - - it 'finds records created between `from` and `to` range' do - options[:from] = 10.days.ago - options[:to] = 10.days.from_now - - # 2 deployments over 20 days - expect(subject).to eq('0.1') - end - - context 'when `from` and `to` are within a day' do - it 'returns the number of deployments made on that day' do - freeze_time do - create(:deployment, :success, project: project, finished_at: Time.zone.now) - options[:from] = Time.zone.now.at_beginning_of_day - options[:to] = Time.zone.now.at_end_of_day - - expect(subject).to eq('1') - end - end - end - end - end + it_behaves_like 'deployment metrics examples' end diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb index 8a26e153385..dcb8138bdde 100644 --- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb +++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do allow(request_context).to receive(:request).and_return(fake_request) allow(project.repository).to receive(:branch_count).and_return(branches.size) - expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches) + expect(finder).to receive(:execute).and_return(branches) expect(request_context).to receive(:header).with('X-Per-Page', '2') expect(request_context).to receive(:header).with('X-Page', '1') expect(request_context).to receive(:header).with('X-Next-Page', '2') @@ -99,6 +99,7 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do before do allow(request_context).to receive(:request).and_return(fake_request) + allow(finder).to receive(:is_a?).with(BranchesFinder) { true } expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches) end diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb index 90b39791bf5..698885ddcf4 100644 --- a/spec/requests/api/npm_instance_packages_spec.rb +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -10,39 +10,27 @@ RSpec.describe API::NpmInstancePackages do include_context 'npm api setup' - shared_examples 'handling all endpoints' do - describe 'GET /api/v4/packages/npm/*package_name' do - it_behaves_like 'handling get metadata requests', scope: :instance do - let(:url) { api("/packages/npm/#{package_name}") } - end - end - - describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do - it_behaves_like 'handling get dist tags requests', scope: :instance do - let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") } - end + describe 'GET /api/v4/packages/npm/*package_name' do + it_behaves_like 'handling get metadata requests', scope: :instance do + let(:url) { api("/packages/npm/#{package_name}") } end + end - describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do - it_behaves_like 'handling create dist tag requests', scope: :instance do - let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } - end + describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do + it_behaves_like 'handling get dist tags requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") } end + end - describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do - it_behaves_like 'handling delete dist tag requests', scope: :instance do - let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } - end + describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling create dist tag requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end end - it_behaves_like 'handling all endpoints' - - context 'with npm_finder_query_avoid_duplicated_conditions disabled' do - before do - stub_feature_flags(npm_finder_query_avoid_duplicated_conditions: false) + describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling delete dist tag requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } end - - it_behaves_like 'handling all endpoints' end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 8c35a1642e2..0d04c2cad5b 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -120,9 +120,11 @@ RSpec.describe API::NpmProjectPackages do project.add_developer(user) end + subject(:upload_package_with_token) { upload_with_token(package_name, params) } + shared_examples 'handling invalid record with 400 error' do it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do - expect { upload_package_with_token(package_name, params) } + expect { upload_package_with_token } .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:bad_request) @@ -136,6 +138,7 @@ RSpec.describe API::NpmProjectPackages do let(:params) { upload_params(package_name: package_name) } it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'not a package tracking event' end context 'invalid package version' do @@ -157,6 +160,7 @@ RSpec.describe API::NpmProjectPackages do let(:params) { upload_params(package_name: package_name, package_version: version) } it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'not a package tracking event' end end end @@ -169,8 +173,6 @@ RSpec.describe API::NpmProjectPackages do shared_examples 'handling upload with different authentications' do context 'with access token' do - subject { upload_package_with_token(package_name, params) } - it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package' it 'creates npm package with file' do @@ -184,7 +186,7 @@ RSpec.describe API::NpmProjectPackages do end it 'creates npm package with file with job token' do - expect { upload_package_with_job_token(package_name, params) } + expect { upload_with_job_token(package_name, params) } .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) @@ -205,7 +207,7 @@ RSpec.describe API::NpmProjectPackages do end it 'creates the package metadata' do - upload_package_with_token(package_name, params) + upload_package_with_token expect(response).to have_gitlab_http_status(:ok) expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline @@ -215,7 +217,7 @@ RSpec.describe API::NpmProjectPackages do shared_examples 'uploading the package' do it 'uploads the package' do - expect { upload_package_with_token(package_name, params) } + expect { upload_package_with_token } .to change { project.packages.count }.by(1) expect(response).to have_gitlab_http_status(:ok) @@ -249,6 +251,7 @@ RSpec.describe API::NpmProjectPackages do let(:package_name) { "@#{group.path}/test" } it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'not a package tracking event' context 'with a new version' do let_it_be(:version) { '4.5.6' } @@ -271,9 +274,14 @@ RSpec.describe API::NpmProjectPackages do let(:package_name) { "@#{group.path}/my_package_name" } let(:params) { upload_params(package_name: package_name) } - it 'returns an error if the package already exists' do + before do create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name") - expect { upload_package_with_token(package_name, params) } + end + + it_behaves_like 'not a package tracking event' + + it 'returns an error if the package already exists' do + expect { upload_package_with_token } .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:forbidden) @@ -285,7 +293,7 @@ RSpec.describe API::NpmProjectPackages do let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') } it 'creates npm package with file and dependencies' do - expect { upload_package_with_token(package_name, params) } + expect { upload_package_with_token } .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) .and change { Packages::Dependency.count}.by(4) @@ -297,11 +305,11 @@ RSpec.describe API::NpmProjectPackages do context 'with existing dependencies' do before do name = "@#{group.path}/existing_package" - upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json')) + upload_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json')) end it 'reuses them' do - expect { upload_package_with_token(package_name, params) } + expect { upload_package_with_token } .to change { project.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) .and not_change { Packages::Dependency.count} @@ -317,11 +325,11 @@ RSpec.describe API::NpmProjectPackages do put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers end - def upload_package_with_token(package_name, params = {}) + def upload_with_token(package_name, params = {}) upload_package(package_name, params.merge(access_token: token.token)) end - def upload_package_with_job_token(package_name, params = {}) + def upload_with_job_token(package_name, params = {}) upload_package(package_name, params.merge(job_token: job.token)) end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 1b90d6fa47e..a576e1ab1ee 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -22,7 +22,7 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response).to be_an Array + expect(json_response).to be_an(Array) first_commit = json_response.first expect(first_commit['name']).to eq('bar') @@ -73,6 +73,25 @@ RSpec.describe API::Repositories do end end end + + context 'keyset pagination mode' do + let(:first_response) do + get api(route, current_user), params: { pagination: "keyset" } + + Gitlab::Json.parse(response.body) + end + + it 'paginates using keysets' do + page_token = first_response.last["id"] + + get api(route, current_user), params: { pagination: "keyset", page_token: page_token } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response).not_to eq(first_response) + expect(json_response.map { |t| t["id"] }).not_to include(page_token) + end + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb new file mode 100644 index 00000000000..6342064beb8 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +shared_examples 'deployment metrics examples' do + def create_deployment(args) + project = args[:project] + environment = project.environments.production.first || create(:environment, :production, project: project) + create(:deployment, :success, args.merge(environment: environment)) + + # this is needed for the dora_deployment_frequency_in_vsa feature flag so we have aggregated data + ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee? + end + + describe "#deploys" do + subject { stage_summary.third } + + context 'when from date is given' do + before do + travel_to(5.days.ago) { create_deployment(project: project) } + create_deployment(project: project) + end + + it "finds the number of deploys made created after the 'from date'" do + expect(subject[:value]).to eq('1') + end + + it 'returns the localized title' do + Gitlab::I18n.with_locale(:ru) do + expect(subject[:title]).to eq(n_('Deploy', 'Deploys', 1)) + end + end + end + + it "doesn't find commits from other projects" do + travel_to(5.days.from_now) do + create_deployment(project: create(:project, :repository)) + end + + expect(subject[:value]).to eq('-') + end + + context 'when `to` parameter is given' do + before do + travel_to(5.days.ago) { create_deployment(project: project) } + travel_to(5.days.from_now) { create_deployment(project: project) } + end + + it "doesn't find any record" do + options[:to] = Time.now + + expect(subject[:value]).to eq('-') + end + + it "finds records created between `from` and `to` range" do + options[:from] = 10.days.ago + options[:to] = 10.days.from_now + + expect(subject[:value]).to eq('2') + end + end + end + + describe '#deployment_frequency' do + subject { stage_summary.fourth[:value] } + + it 'includes the unit: `per day`' do + expect(stage_summary.fourth[:unit]).to eq _('per day') + end + + before do + travel_to(5.days.ago) { create_deployment(project: project) } + end + + it 'returns 0.0 when there were deploys but the frequency was too low' do + options[:from] = 30.days.ago + + # 1 deployment over 30 days + # frequency of 0.03, rounded off to 0.0 + expect(subject).to eq('0') + end + + it 'returns `-` when there were no deploys' do + options[:from] = 4.days.ago + + # 0 deployment in the last 4 days + expect(subject).to eq('-') + end + + context 'when `to` is nil' do + it 'includes range until now' do + options[:from] = 6.days.ago + options[:to] = nil + + # 1 deployment over 7 days + expect(subject).to eq('0.1') + end + end + + context 'when `to` is given' do + before do + travel_to(5.days.from_now) { create_deployment(project: project, finished_at: Time.zone.now) } + end + + it 'finds records created between `from` and `to` range' do + options[:from] = 10.days.ago + options[:to] = 10.days.from_now + + # 2 deployments over 20 days + expect(subject).to eq('0.1') + end + + context 'when `from` and `to` are within a day' do + it 'returns the number of deployments made on that day' do + freeze_time do + create_deployment(project: project, finished_at: Time.current) + options[:from] = Time.current.yesterday.beginning_of_day + options[:to] = Time.current.end_of_day + + expect(subject).to eq('0.5') + end + end + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index ecde4ee8565..eb650b7a09f 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -153,3 +153,15 @@ RSpec.shared_examples 'a package tracking event' do |category, action| expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) end end + +RSpec.shared_examples 'not a package tracking event' do + before do + stub_feature_flags(collect_package_events: true) + end + + it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do + expect { subject }.not_to change { Packages::Event.count } + + expect_no_snowplow_event + end +end |