From 10130901f1128c91596c4cbfe14b1e5c9f15032f Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 15 Apr 2021 15:09:11 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITALY_SERVER_VERSION | 2 +- GITLAB_PAGES_VERSION | 2 +- app/controllers/concerns/service_params.rb | 2 + .../projects/branches_controller.rb | 3 - app/graphql/resolvers/blobs_resolver.rb | 2 +- .../resolvers/ci/test_suite_resolver.rb | 40 ++++ app/graphql/types/ci/pipeline_type.rb | 6 + app/graphql/types/ci/recent_failures_type.rb | 20 ++ app/graphql/types/ci/test_case_status_enum.rb | 15 ++ app/graphql/types/ci/test_case_type.rb | 41 ++++ app/graphql/types/ci/test_suite_type.rb | 41 ++++ app/graphql/types/repository/blob_type.rb | 40 ++++ app/graphql/types/repository_type.rb | 2 +- app/helpers/branches_helper.rb | 4 - app/helpers/ci/pipelines_helper.rb | 66 +++---- app/helpers/projects_helper.rb | 8 - app/helpers/whats_new_helper.rb | 4 + app/models/ci/pipeline.rb | 6 +- app/models/commit_status.rb | 6 + app/models/concerns/issuable.rb | 8 + app/models/concerns/milestoneable.rb | 4 +- .../sidebars/container_with_html_options.rb | 7 + .../concerns/vulnerability_finding_helpers.rb | 7 + ...vulnerability_finding_signature_helpers.rb | 7 + app/models/pages_deployment.rb | 2 + .../projects/menus/project_overview/menu.rb | 45 +++++ .../project_overview/menu_items/activity.rb | 35 ++++ .../project_overview/menu_items/details.rb | 36 ++++ .../project_overview/menu_items/releases.rb | 40 ++++ app/models/sidebars/projects/panel.rb | 2 + app/services/ci/process_pipeline_service.rb | 2 +- app/services/issuable/bulk_update_service.rb | 13 +- app/services/merge_requests/update_service.rb | 56 ++++-- .../in_product_marketing_emails_service.rb | 8 +- .../migrate_from_legacy_storage_service.rb | 38 ++-- app/views/layouts/header/_default.html.haml | 3 +- .../header/_whats_new_dropdown_item.html.haml | 11 +- .../nav/sidebar/_project_menus.html.haml | 26 --- app/views/projects/branches/index.html.haml | 20 +- app/views/shared/nav/_sidebar.html.haml | 2 + app/views/shared/nav/_sidebar_menu.html.haml | 27 +++ .../shared/nav/_sidebar_menu_item.html.haml | 8 + .../21068-optimize-issueable-updates.yml | 5 + .../unreleased/300121-fix-jenkins-ce.yml | 5 + ...pages-to-zip-storage-in-the-background.yml | 5 + .../325285-rake-pages-deployments.yml | 5 + .../unreleased/be-test-suite-graphql.yml | 5 + .../jivanvl-remove-gldropdown-branches-ff.yml | 5 + ...n_whats_new_self_managed_authenticated.yml | 5 + ...ecialized-service-for-assignee-updates.yml | 5 + ...11-add-queue-to-background-transaction.yml | 5 + changelogs/unreleased/upgrade-pages-1-38.yml | 5 + ....yml => load_balancing_atomic_replica.yml} | 10 +- ...0_schedule_migrate_pages_to_zip_storage.rb | 36 ++++ db/schema_migrations/20210302150310 | 1 + doc/administration/whats-new.md | 29 +++ doc/api/api_resources.md | 3 +- doc/api/graphql/reference/index.md | 112 ++++++++++- doc/api/usage_data.md | 56 +++++- doc/development/usage_ping/index.md | 31 --- doc/integration/jira/connect-app.md | 2 + doc/integration/jira/dvcs.md | 19 ++ doc/user/clusters/agent/index.md | 40 ++-- doc/user/project/integrations/webhooks.md | 1 - .../merge_requests/creating_merge_requests.md | 21 ++ doc/user/project/settings/index.md | 1 + .../migrate_pages_to_zip_storage.rb | 19 ++ lib/gitlab/ci/pipeline/chain/command.rb | 7 +- lib/gitlab/ci/pipeline/chain/helpers.rb | 30 +-- lib/gitlab/ci/pipeline/chain/metrics.rb | 2 +- lib/gitlab/ci/pipeline/metrics.rb | 74 +++---- .../background_migration/batched_migration.rb | 7 + .../batched_migration_wrapper.rb | 62 ++++++ lib/gitlab/metrics/background_transaction.rb | 20 +- lib/gitlab/pages/migration_helper.rb | 53 +++++ lib/gitlab/usage_data_non_sql_metrics.rb | 6 + lib/tasks/gitlab/pages.rake | 33 +++- locale/gitlab.pot | 15 +- qa/qa/page/project/menu.rb | 7 +- qa/qa/page/project/sub_menus/project.rb | 6 +- .../branches/user_deletes_branch_spec.rb | 25 +-- spec/features/projects/branches_spec.rb | 106 ++-------- spec/features/protected_branches_spec.rb | 47 +---- spec/features/whats_new_spec.rb | 66 +++++-- .../resolvers/ci/test_suite_resolver_spec.rb | 54 +++++ spec/graphql/types/ci/pipeline_type_spec.rb | 2 +- .../types/ci/recent_failures_type_spec.rb | 15 ++ .../types/ci/test_case_status_enum_spec.rb | 13 ++ spec/graphql/types/ci/test_case_type_spec.rb | 15 ++ spec/graphql/types/ci/test_suite_type_spec.rb | 15 ++ .../types/repository/blob_type_spec.rb | 9 + spec/helpers/branches_helper_spec.rb | 15 -- spec/helpers/whats_new_helper_spec.rb | 28 +++ .../migrate_pages_to_zip_storage_spec.rb | 43 ++++ .../gitlab/ci/pipeline/chain/command_spec.rb | 21 ++ .../pipeline/chain/limit/deployments_spec.rb | 13 +- .../gitlab/ci/pipeline/chain/populate_spec.rb | 5 + .../batched_migration_spec.rb | 13 ++ .../batched_migration_wrapper_spec.rb | 65 +++++- .../metrics/background_transaction_spec.rb | 51 ++++- .../gitlab/usage_data_non_sql_metrics_spec.rb | 18 ++ ...edule_migrate_pages_to_zip_storage_spec.rb | 46 +++++ spec/models/bulk_imports/stage_spec.rb | 8 +- spec/models/ci/pipeline_spec.rb | 10 + spec/models/commit_status_spec.rb | 17 +- spec/models/concerns/issuable_spec.rb | 17 ++ spec/models/concerns/milestoneable_spec.rb | 14 +- spec/models/pages_deployment_spec.rb | 26 ++- .../menu_items/releases_spec.rb | 38 ++++ .../menus/project_overview/menu_spec.rb | 18 ++ .../api/graphql/project/pipeline_spec.rb | 47 +++++ .../graphql/project/repository/blobs_spec.rb | 36 ++++ .../ci/create_pipeline_service_spec.rb | 19 +- .../ci/process_pipeline_service_spec.rb | 52 ++--- .../merge_requests/update_service_spec.rb | 34 ++++ ...n_product_marketing_emails_service_spec.rb | 56 ++++-- ...igrate_from_legacy_storage_service_spec.rb | 185 ++++++++++-------- spec/tasks/gitlab/pages_rake_spec.rb | 96 +++++++-- .../nav/sidebar/_project.html.haml_spec.rb | 49 +++-- 119 files changed, 2232 insertions(+), 645 deletions(-) create mode 100644 app/graphql/resolvers/ci/test_suite_resolver.rb create mode 100644 app/graphql/types/ci/recent_failures_type.rb create mode 100644 app/graphql/types/ci/test_case_status_enum.rb create mode 100644 app/graphql/types/ci/test_case_type.rb create mode 100644 app/graphql/types/ci/test_suite_type.rb create mode 100644 app/graphql/types/repository/blob_type.rb create mode 100644 app/models/concerns/vulnerability_finding_helpers.rb create mode 100644 app/models/concerns/vulnerability_finding_signature_helpers.rb create mode 100644 app/models/sidebars/projects/menus/project_overview/menu.rb create mode 100644 app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb create mode 100644 app/models/sidebars/projects/menus/project_overview/menu_items/details.rb create mode 100644 app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb create mode 100644 app/views/shared/nav/_sidebar_menu.html.haml create mode 100644 app/views/shared/nav/_sidebar_menu_item.html.haml create mode 100644 changelogs/unreleased/21068-optimize-issueable-updates.yml create mode 100644 changelogs/unreleased/300121-fix-jenkins-ce.yml create mode 100644 changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml create mode 100644 changelogs/unreleased/325285-rake-pages-deployments.yml create mode 100644 changelogs/unreleased/be-test-suite-graphql.yml create mode 100644 changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml create mode 100644 changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml create mode 100644 changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml create mode 100644 changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml create mode 100644 changelogs/unreleased/upgrade-pages-1-38.yml rename config/feature_flags/development/{gldropdown_branches.yml => load_balancing_atomic_replica.yml} (61%) create mode 100644 db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb create mode 100644 db/schema_migrations/20210302150310 create mode 100644 doc/administration/whats-new.md create mode 100644 lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb create mode 100644 lib/gitlab/pages/migration_helper.rb create mode 100644 spec/graphql/resolvers/ci/test_suite_resolver_spec.rb create mode 100644 spec/graphql/types/ci/recent_failures_type_spec.rb create mode 100644 spec/graphql/types/ci/test_case_status_enum_spec.rb create mode 100644 spec/graphql/types/ci/test_case_type_spec.rb create mode 100644 spec/graphql/types/ci/test_suite_type_spec.rb create mode 100644 spec/graphql/types/repository/blob_type_spec.rb create mode 100644 spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb create mode 100644 spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb create mode 100644 spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb create mode 100644 spec/models/sidebars/projects/menus/project_overview/menu_spec.rb create mode 100644 spec/requests/api/graphql/project/repository/blobs_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9657f8ff598..8c745bb4253 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -e505e98295e765f945719f289125a98e671a7e19 +6904387a86815c80988d87f23af9d3fe1e2d4c85 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index bf50e910e62..ebeef2f2d61 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.37.0 +1.38.0 diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 6262d29a734..7c57d321c80 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -44,6 +44,7 @@ module ServiceParams # make those event names plural as special case. :issues_events, :issues_url, + :jenkins_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :manual_configuration, @@ -56,6 +57,7 @@ module ServiceParams :password, :priority, :project_key, + :project_name, :project_url, :recipients, :restrict_to_branch, diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 12510b4eb33..f522dffdf3e 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -12,9 +12,6 @@ class Projects::BranchesController < Projects::ApplicationController # Support legacy URLs before_action :redirect_for_legacy_index_sort_or_search, only: [:index] before_action :limit_diverging_commit_counts!, only: [:diverging_commit_counts] - before_action do - push_frontend_feature_flag(:gldropdown_branches, default_enabled: :yaml) - end feature_category :source_code_management diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb index 521e0482759..d006769bd4b 100644 --- a/app/graphql/resolvers/blobs_resolver.rb +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -21,7 +21,7 @@ module Resolvers # We fetch blobs from Gitaly efficiently but it still scales O(N) with the # number of paths being fetched, so apply a scaling limit to that. def self.resolver_complexity(args, child_complexity:) - super + args.fetch(:paths, []).size + super + (args[:paths] || []).size end def resolve(paths:, ref:) diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb new file mode 100644 index 00000000000..90cc30b1281 --- /dev/null +++ b/app/graphql/resolvers/ci/test_suite_resolver.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class TestSuiteResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type ::Types::Ci::TestSuiteType, null: true + authorize :read_build + authorizes_object! + + alias_method :pipeline, :object + + argument :build_ids, [GraphQL::ID_TYPE], + required: true, + description: 'IDs of the builds used to run the test suite.' + + def resolve(build_ids:) + builds = pipeline.latest_builds.id_in(build_ids).presence + return unless builds + + TestSuiteSerializer + .new(project: pipeline.project, current_user: @current_user) + .represent(load_test_suite_data(builds), details: true) + end + + private + + def load_test_suite_data(builds) + suite = builds.sum do |build| + build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + end + + Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load! + + suite + end + end + end +end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 79b1271c19f..2e83f6c1f5a 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -128,6 +128,12 @@ module Types description: 'Summary of the test report generated by the pipeline.', resolver: Resolvers::Ci::TestReportSummaryResolver + field :test_suite, + Types::Ci::TestSuiteType, + null: true, + description: 'A specific test suite in a pipeline test report.', + resolver: Resolvers::Ci::TestSuiteResolver + def detailed_status object.detailed_status(current_user) end diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb new file mode 100644 index 00000000000..eeff7222762 --- /dev/null +++ b/app/graphql/types/ci/recent_failures_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RecentFailuresType < BaseObject + graphql_name 'RecentFailures' + description 'Recent failure history of a test case.' + + connection_type_class(Types::CountableConnectionType) + + field :count, GraphQL::INT_TYPE, null: true, + description: 'Number of times the test case has failed in the past 14 days.' + + field :base_branch, GraphQL::STRING_TYPE, null: true, + description: 'Name of the base branch of the project.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_case_status_enum.rb b/app/graphql/types/ci/test_case_status_enum.rb new file mode 100644 index 00000000000..6a5f8bc2a59 --- /dev/null +++ b/app/graphql/types/ci/test_case_status_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + class TestCaseStatusEnum < BaseEnum + graphql_name 'TestCaseStatus' + + ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status| + value status, + description: "Test case that has a status of #{status}.", + value: status + end + end + end +end diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb new file mode 100644 index 00000000000..9cc3f918125 --- /dev/null +++ b/app/graphql/types/ci/test_case_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestCaseType < BaseObject + graphql_name 'TestCase' + description 'Test case in pipeline test report.' + + connection_type_class(Types::CountableConnectionType) + + field :status, Types::Ci::TestCaseStatusEnum, null: true, + description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})." + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the test case.' + + field :classname, GraphQL::STRING_TYPE, null: true, + description: 'Classname of the test case.' + + field :execution_time, GraphQL::FLOAT_TYPE, null: true, + description: 'Test case execution time in seconds.' + + field :file, GraphQL::STRING_TYPE, null: true, + description: 'Path to the file of the test case.' + + field :attachment_url, GraphQL::STRING_TYPE, null: true, + description: 'URL of the test case attachment file.' + + field :system_output, GraphQL::STRING_TYPE, null: true, + description: 'System output of the test case.' + + field :stack_trace, GraphQL::STRING_TYPE, null: true, + description: 'Stack trace of the test case.' + + field :recent_failures, Types::Ci::RecentFailuresType, null: true, + description: 'Recent failure history of the test case on the base branch.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb new file mode 100644 index 00000000000..7d4c01da81b --- /dev/null +++ b/app/graphql/types/ci/test_suite_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class TestSuiteType < BaseObject + graphql_name 'TestSuite' + description 'Test suite in a pipeline test report.' + + connection_type_class(Types::CountableConnectionType) + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the test suite.' + + field :total_time, GraphQL::FLOAT_TYPE, null: true, + description: 'Total duration of the tests in the test suite.' + + field :total_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of the test cases in the test suite.' + + field :success_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that succeeded in the test suite.' + + field :failed_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that failed in the test suite.' + + field :skipped_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that were skipped in the test suite.' + + field :error_count, GraphQL::INT_TYPE, null: true, + description: 'Total number of test cases that had an error.' + + field :suite_error, GraphQL::STRING_TYPE, null: true, + description: 'Test suite error message.' + + field :test_cases, Types::Ci::TestCaseType.connection_type, null: true, + description: 'Test cases in the test suite.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb new file mode 100644 index 00000000000..912fc5f643a --- /dev/null +++ b/app/graphql/types/repository/blob_type.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module Types + module Repository + # rubocop: disable Graphql/AuthorizeTypes + # This is presented through `Repository` that has its own authorization + class BlobType < BaseObject + present_using BlobPresenter + + graphql_name 'RepositoryBlob' + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the blob.' + + field :oid, GraphQL::STRING_TYPE, null: false, method: :id, + description: 'OID of the blob.' + + field :path, GraphQL::STRING_TYPE, null: false, + description: 'Path of the blob.' + + field :name, GraphQL::STRING_TYPE, + description: 'Blob name.', + null: true + + field :mode, type: GraphQL::STRING_TYPE, + description: 'Blob mode.', + null: true + + field :lfs_oid, GraphQL::STRING_TYPE, null: true, + calls_gitaly: true, + description: 'LFS OID of the blob.' + + field :web_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path of the blob.' + + def lfs_oid + Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find + end + end + end +end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index d426868b129..963a4296c4f 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -14,7 +14,7 @@ module Types description: 'Indicates a corresponding Git repository exists on disk.' field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, description: 'Tree of the repository.' - field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, + field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, description: 'Blobs contained within the repository' field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true, complexity: 170, description: 'Names of branches available in this repository that match the search pattern.', diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index ef855718c43..8f87cd5bfe0 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -20,10 +20,6 @@ module BranchesHelper end end end - - def gldropdrown_branches_enabled? - Feature.enabled?(:gldropdown_branches, default_enabled: :yaml) - end end BranchesHelper.prepend_if_ee('EE::BranchesHelper') diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb index 766c46249eb..cabb43f45fd 100644 --- a/app/helpers/ci/pipelines_helper.rb +++ b/app/helpers/ci/pipelines_helper.rb @@ -34,39 +34,39 @@ module Ci # and will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/326299 def experiment_suggested_ci_templates [ - { name: 'Android', logo: image_path('/assets/illustrations/logos/android.svg') }, - { name: 'Bash', logo: image_path('/assets/illustrations/logos/bash.svg') }, - { name: 'C++', logo: image_path('/assets/illustrations/logos/c_plus_plus.svg') }, - { name: 'Clojure', logo: image_path('/assets/illustrations/logos/clojure.svg') }, - { name: 'Composer', logo: image_path('/assets/illustrations/logos/composer.svg') }, - { name: 'Crystal', logo: image_path('/assets/illustrations/logos/crystal.svg') }, - { name: 'Dart', logo: image_path('/assets/illustrations/logos/dart.svg') }, - { name: 'Django', logo: image_path('/assets/illustrations/logos/django.svg') }, - { name: 'Docker', logo: image_path('/assets/illustrations/logos/docker.svg') }, - { name: 'Elixir', logo: image_path('/assets/illustrations/logos/elixir.svg') }, - { name: 'iOS-Fastlane', logo: image_path('/assets/illustrations/logos/fastlane.svg') }, - { name: 'Flutter', logo: image_path('/assets/illustrations/logos/flutter.svg') }, - { name: 'Go', logo: image_path('/assets/illustrations/logos/go_logo.svg') }, - { name: 'Gradle', logo: image_path('/assets/illustrations/logos/gradle.svg') }, - { name: 'Grails', logo: image_path('/assets/illustrations/logos/grails.svg') }, - { name: 'dotNET', logo: image_path('/assets/illustrations/logos/dotnet.svg') }, - { name: 'Rails', logo: image_path('/assets/illustrations/logos/rails.svg') }, - { name: 'Julia', logo: image_path('/assets/illustrations/logos/julia.svg') }, - { name: 'Laravel', logo: image_path('/assets/illustrations/logos/laravel.svg') }, - { name: 'Latex', logo: image_path('/assets/illustrations/logos/latex.svg') }, - { name: 'Maven', logo: image_path('/assets/illustrations/logos/maven.svg') }, - { name: 'Mono', logo: image_path('/assets/illustrations/logos/mono.svg') }, - { name: 'Nodejs', logo: image_path('/assets/illustrations/logos/node_js.svg') }, - { name: 'npm', logo: image_path('/assets/illustrations/logos/npm.svg') }, - { name: 'OpenShift', logo: image_path('/assets/illustrations/logos/openshift.svg') }, - { name: 'Packer', logo: image_path('/assets/illustrations/logos/packer.svg') }, - { name: 'PHP', logo: image_path('/assets/illustrations/logos/php.svg') }, - { name: 'Python', logo: image_path('/assets/illustrations/logos/python.svg') }, - { name: 'Ruby', logo: image_path('/assets/illustrations/logos/ruby.svg') }, - { name: 'Rust', logo: image_path('/assets/illustrations/logos/rust.svg') }, - { name: 'Scala', logo: image_path('/assets/illustrations/logos/scala.svg') }, - { name: 'Swift', logo: image_path('/assets/illustrations/logos/swift.svg') }, - { name: 'Terraform', logo: image_path('/assets/illustrations/logos/terraform.svg') } + { name: 'Android', logo: image_path('illustrations/logos/android.svg') }, + { name: 'Bash', logo: image_path('illustrations/logos/bash.svg') }, + { name: 'C++', logo: image_path('illustrations/logos/c_plus_plus.svg') }, + { name: 'Clojure', logo: image_path('illustrations/logos/clojure.svg') }, + { name: 'Composer', logo: image_path('illustrations/logos/composer.svg') }, + { name: 'Crystal', logo: image_path('illustrations/logos/crystal.svg') }, + { name: 'Dart', logo: image_path('illustrations/logos/dart.svg') }, + { name: 'Django', logo: image_path('illustrations/logos/django.svg') }, + { name: 'Docker', logo: image_path('illustrations/logos/docker.svg') }, + { name: 'Elixir', logo: image_path('illustrations/logos/elixir.svg') }, + { name: 'iOS-Fastlane', logo: image_path('illustrations/logos/fastlane.svg') }, + { name: 'Flutter', logo: image_path('illustrations/logos/flutter.svg') }, + { name: 'Go', logo: image_path('illustrations/logos/go_logo.svg') }, + { name: 'Gradle', logo: image_path('illustrations/logos/gradle.svg') }, + { name: 'Grails', logo: image_path('illustrations/logos/grails.svg') }, + { name: 'dotNET', logo: image_path('illustrations/logos/dotnet.svg') }, + { name: 'Rails', logo: image_path('illustrations/logos/rails.svg') }, + { name: 'Julia', logo: image_path('illustrations/logos/julia.svg') }, + { name: 'Laravel', logo: image_path('illustrations/logos/laravel.svg') }, + { name: 'Latex', logo: image_path('illustrations/logos/latex.svg') }, + { name: 'Maven', logo: image_path('illustrations/logos/maven.svg') }, + { name: 'Mono', logo: image_path('illustrations/logos/mono.svg') }, + { name: 'Nodejs', logo: image_path('illustrations/logos/node_js.svg') }, + { name: 'npm', logo: image_path('illustrations/logos/npm.svg') }, + { name: 'OpenShift', logo: image_path('illustrations/logos/openshift.svg') }, + { name: 'Packer', logo: image_path('illustrations/logos/packer.svg') }, + { name: 'PHP', logo: image_path('illustrations/logos/php.svg') }, + { name: 'Python', logo: image_path('illustrations/logos/python.svg') }, + { name: 'Ruby', logo: image_path('illustrations/logos/ruby.svg') }, + { name: 'Rust', logo: image_path('illustrations/logos/rust.svg') }, + { name: 'Scala', logo: image_path('illustrations/logos/scala.svg') }, + { name: 'Swift', logo: image_path('illustrations/logos/swift.svg') }, + { name: 'Terraform', logo: image_path('illustrations/logos/terraform.svg') } ] end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 70c094c7ec6..68c3738d19e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -729,14 +729,6 @@ module ProjectsHelper ] end - def sidebar_projects_paths - %w[ - projects#show - projects#activity - releases#index - ] - end - def sidebar_settings_paths %w[ projects#edit diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index 23ed2fc987c..9362ae1491f 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -8,4 +8,8 @@ module WhatsNewHelper def whats_new_version_digest ReleaseHighlight.most_recent_version_digest end + + def display_whats_new? + Gitlab.dev_env_org_or_com? || user_signed_in? + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0e55841f5fa..c9ab69317e1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -286,9 +286,11 @@ module Ci end after_transition any => [:failed] do |pipeline| - next unless pipeline.auto_devops_source? + pipeline.run_after_commit do + ::Gitlab::Ci::Pipeline::Metrics.pipeline_failure_reason_counter.increment(reason: pipeline.failure_reason) - pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) } + AutoDevops::DisableWorker.perform_async(pipeline.id) if pipeline.auto_devops_source? + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c89a132bdd6..e989129209a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -179,6 +179,12 @@ class CommitStatus < ApplicationRecord ExpireJobCacheWorker.perform_async(id) end end + + after_transition any => :failed do |commit_status| + commit_status.run_after_commit do + ::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter.increment(reason: commit_status.failure_reason) + end + end end def self.names diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 478c7cd156f..1e44321e148 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -137,6 +137,14 @@ module Issuable scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } + scope :includes_for_bulk_update, -> do + associations = %i[author assignees epic group labels metrics project source_project target_project].select do |association| + reflect_on_association(association) + end + + includes(*associations) + end + attr_mentionable :title, pipeline: :single_line attr_mentionable :description diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index ccb334343ff..d42417bb6c1 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -39,11 +39,13 @@ module Milestoneable private def milestone_is_valid - errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available? + errors.add(:milestone_id, 'is invalid') if respond_to?(:milestone_id) && !milestone_available? end end def milestone_available? + return true if milestone_id.blank? + project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group) end diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb index 911540f25f8..8cb2fc7d6b2 100644 --- a/app/models/concerns/sidebars/container_with_html_options.rb +++ b/app/models/concerns/sidebars/container_with_html_options.rb @@ -17,6 +17,13 @@ module Sidebars {} end + # Attributes to pass to the html_options attribute + # in the helper method that sets the active class + # on each element. + def nav_link_html_options + {} + end + def title raise NotImplementedError end diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb new file mode 100644 index 00000000000..cf50305faab --- /dev/null +++ b/app/models/concerns/vulnerability_finding_helpers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module VulnerabilityFindingHelpers + extend ActiveSupport::Concern +end + +VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers') diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb new file mode 100644 index 00000000000..f57e3cb0bfb --- /dev/null +++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module VulnerabilityFindingSignatureHelpers + extend ActiveSupport::Concern +end + +VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers') diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index d67a92af6af..294a4e85d1f 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -14,6 +14,8 @@ class PagesDeployment < ApplicationRecord scope :older_than, -> (id) { where('id < ?', id) } scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) } + scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) } + scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) } validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } diff --git a/app/models/sidebars/projects/menus/project_overview/menu.rb b/app/models/sidebars/projects/menus/project_overview/menu.rb new file mode 100644 index 00000000000..e6aa8ed159f --- /dev/null +++ b/app/models/sidebars/projects/menus/project_overview/menu.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + module ProjectOverview + class Menu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + add_item(MenuItems::Details.new(context)) + add_item(MenuItems::Activity.new(context)) + add_item(MenuItems::Releases.new(context)) + end + + override :link + def link + project_path(context.project) + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-project rspec-project-link' + } + end + + override :extra_container_html_options + def nav_link_html_options + { class: 'home' } + end + + override :title + def title + _('Project overview') + end + + override :sprite_icon + def sprite_icon + 'home' + end + end + end + end + end +end diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb new file mode 100644 index 00000000000..46d0f0bc43b --- /dev/null +++ b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + module ProjectOverview + module MenuItems + class Activity < ::Sidebars::MenuItem + override :link + def link + activity_project_path(context.project) + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-project-activity' + } + end + + override :active_routes + def active_routes + { path: 'projects#activity' } + end + + override :title + def title + _('Activity') + end + end + end + end + end + end +end diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb new file mode 100644 index 00000000000..c8cf5d503ab --- /dev/null +++ b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + module ProjectOverview + module MenuItems + class Details < ::Sidebars::MenuItem + override :link + def link + project_path(context.project) + end + + override :extra_container_html_options + def extra_container_html_options + { + title: _('Project details'), + class: 'shortcuts-project' + } + end + + override :active_routes + def active_routes + { path: 'projects#show' } + end + + override :title + def title + _('Details') + end + end + end + end + end + end +end diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb new file mode 100644 index 00000000000..5e8348f4398 --- /dev/null +++ b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + module ProjectOverview + module MenuItems + class Releases < ::Sidebars::MenuItem + override :link + def link + project_releases_path(context.project) + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-project-releases' + } + end + + override :render? + def render? + can?(context.current_user, :read_release, context.project) && !context.project.empty_repo? + end + + override :active_routes + def active_routes + { controller: :releases } + end + + override :title + def title + _('Releases') + end + end + end + end + end + end +end diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb index 2532a151ed6..5f4c7f32164 100644 --- a/app/models/sidebars/projects/panel.rb +++ b/app/models/sidebars/projects/panel.rb @@ -6,6 +6,8 @@ module Sidebars override :configure_menus def configure_menus set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context)) + + add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context)) end override :render_raw_menus_partial diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 970652b4da3..6c69df0c616 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -19,7 +19,7 @@ module Ci end def metrics - @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics end private diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index d3d543edcd7..13e289716ef 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -15,7 +15,7 @@ module Issuable set_update_params(type) items = update_issuables(type, ids) - response_success(payload: { count: items.count }) + response_success(payload: { count: items.size }) rescue ArgumentError => e response_error(e.message, 422) end @@ -59,10 +59,17 @@ module Issuable def find_issuables(parent, model_class, ids) if parent.is_a?(Project) - model_class.id_in(ids).of_projects(parent) + projects = parent elsif parent.is_a?(Group) - model_class.id_in(ids).of_projects(parent.all_projects) + projects = parent.all_projects + else + return end + + model_class + .id_in(ids) + .of_projects(projects) + .includes_for_bulk_update end def response_success(message: nil, payload: nil) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 89202edd0d4..8995c5f2411 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,18 +11,7 @@ module MergeRequests end def execute(merge_request) - # We don't allow change of source/target projects and source branch - # after merge request was created - params.delete(:source_project_id) - params.delete(:target_project_id) - params.delete(:source_branch) - - if merge_request.closed_or_merged_without_fork? - params.delete(:target_branch) - params.delete(:force_remove_source_branch) - end - - update_task_event(merge_request) || update(merge_request) + update_merge_request_with_specialized_service(merge_request) || general_fallback(merge_request) end def handle_changes(merge_request, options) @@ -86,6 +75,21 @@ module MergeRequests attr_reader :target_branch_was_deleted + def general_fallback(merge_request) + # We don't allow change of source/target projects and source branch + # after merge request was created + params.delete(:source_project_id) + params.delete(:target_project_id) + params.delete(:source_branch) + + if merge_request.closed_or_merged_without_fork? + params.delete(:target_branch) + params.delete(:force_remove_source_branch) + end + + update_task_event(merge_request) || update(merge_request) + end + def track_title_and_desc_edits(changed_fields) tracked_fields = %w(title description) @@ -272,6 +276,34 @@ module MergeRequests def quick_action_options { merge_request_diff_head_sha: params.delete(:merge_request_diff_head_sha) } end + + def update_merge_request_with_specialized_service(merge_request) + return unless params.delete(:use_specialized_service) + + # If we're attempting to modify only a single attribute, look up whether + # we have a specialized, targeted service we should use instead. We may + # in the future extend this to include specialized services that operate + # on multiple attributes, but for now limit to only single attribute + # updates. + # + return unless params.one? + + attempt_specialized_update_services(merge_request, params.each_key.first.to_sym) + end + + def attempt_specialized_update_services(merge_request, attribute) + case attribute + when :assignee_ids + assignees_service.execute(merge_request) + else + nil + end + end + + def assignees_service + @assignees_service ||= ::MergeRequests::UpdateAssigneesService + .new(project, current_user, params) + end end end diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb index 2b3c0c382a8..eb81253bc08 100644 --- a/app/services/namespaces/in_product_marketing_emails_service.rb +++ b/app/services/namespaces/in_product_marketing_emails_service.rb @@ -41,9 +41,11 @@ module Namespaces attr_reader :track, :interval, :in_product_marketing_email_records def send_email_for_group(group) - experiment_enabled_for_group = experiment_enabled_for_group?(group) - experiment_add_group(group, experiment_enabled_for_group) - return unless experiment_enabled_for_group + if Gitlab.com? + experiment_enabled_for_group = experiment_enabled_for_group?(group) + experiment_add_group(group, experiment_enabled_for_group) + return unless experiment_enabled_for_group + end users_for_group(group).each do |user| if can_perform_action?(user, group) diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb index d649505f27d..b6aa08bba01 100644 --- a/app/services/pages/migrate_from_legacy_storage_service.rb +++ b/app/services/pages/migrate_from_legacy_storage_service.rb @@ -2,10 +2,8 @@ module Pages class MigrateFromLegacyStorageService - def initialize(logger, migration_threads:, batch_size:, ignore_invalid_entries:, mark_projects_as_not_deployed:) + def initialize(logger, ignore_invalid_entries:, mark_projects_as_not_deployed:) @logger = logger - @migration_threads = migration_threads - @batch_size = batch_size @ignore_invalid_entries = ignore_invalid_entries @mark_projects_as_not_deployed = mark_projects_as_not_deployed @@ -14,25 +12,35 @@ module Pages @counters_lock = Mutex.new end - def execute + def execute_with_threads(threads:, batch_size:) @queue = SizedQueue.new(1) - threads = start_migration_threads + migration_threads = start_migration_threads(threads) - ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: @batch_size) do |batch| + ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: batch_size) do |batch| @queue.push(batch) end @queue.close - @logger.info("Waiting for threads to finish...") - threads.each(&:join) + @logger.info(message: "Pages legacy storage migration: Waiting for threads to finish...") + migration_threads.each(&:join) { migrated: @migrated, errored: @errored } end - def start_migration_threads - Array.new(@migration_threads) do + def execute_for_batch(project_ids) + batch = ProjectPagesMetadatum.only_on_legacy_storage.where(project_id: project_ids) # rubocop: disable CodeReuse/ActiveRecord + + process_batch(batch) + + { migrated: @migrated, errored: @errored } + end + + private + + def start_migration_threads(count) + Array.new(count) do Thread.new do while batch = @queue.pop Rails.application.executor.wrap do @@ -50,12 +58,12 @@ module Pages migrate_project(project) end - @logger.info("#{@migrated} projects are migrated successfully, #{@errored} projects failed to be migrated") + @logger.info(message: "Pages legacy storage migration: batch processed", migrated: @migrated, errored: @errored) rescue => e # This method should never raise exception otherwise all threads might be killed # and this will result in queue starving (and deadlock) Gitlab::ErrorTracking.track_exception(e) - @logger.error("failed processing a batch: #{e.message}") + @logger.error(message: "Pages legacy storage migration: failed processing a batch: #{e.message}") end def migrate_project(project) @@ -67,15 +75,15 @@ module Pages end if result[:status] == :success - @logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds: #{result[:message]}") + @logger.info(message: "Pages legacy storage migration: project migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2)) @counters_lock.synchronize { @migrated += 1 } else - @logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}") + @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2)) @counters_lock.synchronize { @errored += 1 } end rescue => e @counters_lock.synchronize { @errored += 1 } - @logger.error("project_id: #{project&.id} #{project&.pages_path} failed to be migrated: #{e.message}") + @logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project&.id, pages_path: project&.pages_path) Gitlab::ErrorTracking.track_exception(e, project_id: project&.id) end end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3225dad5d57..481e83c9701 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -120,7 +120,8 @@ = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') -#whats-new-app{ data: { version_digest: whats_new_version_digest } } +- if display_whats_new? + #whats-new-app{ data: { version_digest: whats_new_version_digest } } - if can?(current_user, :update_user_status, current_user) .js-set-status-modal-wrapper{ data: user_status_data } diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml index 61fe2f1e711..9fe98a54aae 100644 --- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml +++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml @@ -1,5 +1,6 @@ -%li - %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' } - = _("What's new") - %span.js-whats-new-notification-count.whats-new-notification-count - = whats_new_most_recent_release_items_count +- if display_whats_new? + %li + %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' } + = _("What's new") + %span.js-whats-new-notification-count.whats-new-notification-count + = whats_new_most_recent_release_items_count diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml index 79a7150e030..aee506ead6c 100644 --- a/app/views/layouts/nav/sidebar/_project_menus.html.haml +++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml @@ -1,29 +1,3 @@ -= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do - = link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do - .nav-icon-container - = sprite_icon('home') - %span.nav-item-name - = _('Project overview') - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do - = link_to project_path(@project) do - %strong.fly-out-top-item-name - = _('Project overview') - %li.divider.fly-out-top-item - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do - %span= _('Activity') - - - if project_nav_tab?(:releases) - = nav_link(controller: :releases) do - = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do - %span= _('Releases') - - if project_nav_tab? :learn_gitlab = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do = link_to project_learn_gitlab_path(@project) do diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index c8a5908018d..129b207a26f 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -16,25 +16,7 @@ = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches') .nav-controls - - if !gldropdrown_branches_enabled? - = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do - = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } - - - unless @mode == 'overview' - .dropdown.inline> - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - = branches_sort_options_hash[@sort] - = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") - %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable - %li.dropdown-header - = s_('Branches|Sort by') - - branches_sort_options_hash.each do |value, title| - %li - = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value) - - - else - #js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } } + #js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } } - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml index ab198edb63b..1c06fc9eebf 100644 --- a/app/views/shared/nav/_sidebar.html.haml +++ b/app/views/shared/nav/_sidebar.html.haml @@ -6,6 +6,8 @@ = render sidebar.render_raw_scope_menu_partial %ul.sidebar-top-level-items.qa-project-sidebar + - if sidebar.renderable_menus.any? + = render partial: 'shared/nav/sidebar_menu', collection: sidebar.renderable_menus - if sidebar.render_raw_menus_partial = render sidebar.render_raw_menus_partial diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml new file mode 100644 index 00000000000..cd3222294f3 --- /dev/null +++ b/app/views/shared/nav/_sidebar_menu.html.haml @@ -0,0 +1,27 @@ += nav_link(**sidebar_menu.all_active_routes, html_options: sidebar_menu.nav_link_html_options) do + = link_to sidebar_menu.link, **sidebar_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: sidebar_menu.title } do + - if sidebar_menu.icon_or_image? + .nav-icon-container + - if sidebar_menu.image_path + = image_tag(sidebar_menu.image_path, **sidebar_menu.image_html_options) + - elsif sidebar_menu.sprite_icon + = sprite_icon(sidebar_menu.sprite_icon, **sidebar_menu.sprite_icon_html_options) + + %span.nav-item-name{ **sidebar_menu.title_html_options } + = sidebar_menu.title + - if sidebar_menu.has_pill? + %span.badge.badge-pill.count{ **sidebar_menu.pill_html_options } + = number_with_delimiter(sidebar_menu.pill_count) + + %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_items?) } + = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do + = link_to sidebar_menu.link, title: sidebar_menu.title do + %strong.fly-out-top-item-name + = sidebar_menu.title + - if sidebar_menu.has_pill? + %span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options } + = number_with_delimiter(sidebar_menu.pill_count) + + - if sidebar_menu.has_renderable_items? + %li.divider.fly-out-top-item + = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items diff --git a/app/views/shared/nav/_sidebar_menu_item.html.haml b/app/views/shared/nav/_sidebar_menu_item.html.haml new file mode 100644 index 00000000000..0b0e4c7aec9 --- /dev/null +++ b/app/views/shared/nav/_sidebar_menu_item.html.haml @@ -0,0 +1,8 @@ += nav_link(**sidebar_menu_item.active_routes) do + = link_to sidebar_menu_item.link, **sidebar_menu_item.container_html_options, data: { qa_selector: 'sidebar_menu_item_link', qa_menu_item: sidebar_menu_item.title } do + %span + = sidebar_menu_item.title + - if sidebar_menu_item.sprite_icon + = sprite_icon(sidebar_menu_item.sprite_icon, **sidebar_menu_item.sprite_icon_html_options) + - if sidebar_menu_item.show_hint? + .js-feature-highlight{ **sidebar_menu_item.hint_html_options } diff --git a/changelogs/unreleased/21068-optimize-issueable-updates.yml b/changelogs/unreleased/21068-optimize-issueable-updates.yml new file mode 100644 index 00000000000..225604a9917 --- /dev/null +++ b/changelogs/unreleased/21068-optimize-issueable-updates.yml @@ -0,0 +1,5 @@ +--- +title: Optimize issuable updates +merge_request: 59468 +author: +type: performance diff --git a/changelogs/unreleased/300121-fix-jenkins-ce.yml b/changelogs/unreleased/300121-fix-jenkins-ce.yml new file mode 100644 index 00000000000..03a7efce5fb --- /dev/null +++ b/changelogs/unreleased/300121-fix-jenkins-ce.yml @@ -0,0 +1,5 @@ +--- +title: Fix Jenkins integration for GitLab FOSS +merge_request: 59476 +author: +type: fixed diff --git a/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml b/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml new file mode 100644 index 00000000000..cfa15a3319f --- /dev/null +++ b/changelogs/unreleased/322001-poc-for-migrating-pages-to-zip-storage-in-the-background.yml @@ -0,0 +1,5 @@ +--- +title: Automatically try to migrate gitlab pages to zip storage +merge_request: 54578 +author: +type: added diff --git a/changelogs/unreleased/325285-rake-pages-deployments.yml b/changelogs/unreleased/325285-rake-pages-deployments.yml new file mode 100644 index 00000000000..4647ac4c43f --- /dev/null +++ b/changelogs/unreleased/325285-rake-pages-deployments.yml @@ -0,0 +1,5 @@ +--- +title: Add rake tasks for Pages deployment migration +merge_request: 57120 +author: +type: added diff --git a/changelogs/unreleased/be-test-suite-graphql.yml b/changelogs/unreleased/be-test-suite-graphql.yml new file mode 100644 index 00000000000..e333ea84512 --- /dev/null +++ b/changelogs/unreleased/be-test-suite-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add GraphQL endpoint for a specific test suite in pipelines +merge_request: 58924 +author: +type: added diff --git a/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml b/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml new file mode 100644 index 00000000000..344642e281b --- /dev/null +++ b/changelogs/unreleased/jivanvl-remove-gldropdown-branches-ff.yml @@ -0,0 +1,5 @@ +--- +title: Remove gldropdown_branches feature flag +merge_request: 59179 +author: +type: changed diff --git a/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml b/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml new file mode 100644 index 00000000000..e7e91dd8580 --- /dev/null +++ b/changelogs/unreleased/jswain_whats_new_self_managed_authenticated.yml @@ -0,0 +1,5 @@ +--- +title: Hide What's New for unauthenticated users +merge_request: 59330 +author: +type: changed diff --git a/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml b/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml new file mode 100644 index 00000000000..d251af4a6f9 --- /dev/null +++ b/changelogs/unreleased/kerrizor-use-specialized-service-for-assignee-updates.yml @@ -0,0 +1,5 @@ +--- +title: Add framework for using specialized services to improve performance of MergeRequests::UpdateService +merge_request: 58836 +author: +type: performance diff --git a/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml b/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml new file mode 100644 index 00000000000..9052df1fa01 --- /dev/null +++ b/changelogs/unreleased/qmnguyen0711-add-queue-to-background-transaction.yml @@ -0,0 +1,5 @@ +--- +title: Add queue label to metrics dispatched by background transaction +merge_request: 59344 +author: +type: changed diff --git a/changelogs/unreleased/upgrade-pages-1-38.yml b/changelogs/unreleased/upgrade-pages-1-38.yml new file mode 100644 index 00000000000..ec132f0a508 --- /dev/null +++ b/changelogs/unreleased/upgrade-pages-1-38.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade GitLab Pages to 1.38.0 +merge_request: 59464 +author: +type: added diff --git a/config/feature_flags/development/gldropdown_branches.yml b/config/feature_flags/development/load_balancing_atomic_replica.yml similarity index 61% rename from config/feature_flags/development/gldropdown_branches.yml rename to config/feature_flags/development/load_balancing_atomic_replica.yml index 67d52a495a8..fb0707849d4 100644 --- a/config/feature_flags/development/gldropdown_branches.yml +++ b/config/feature_flags/development/load_balancing_atomic_replica.yml @@ -1,8 +1,8 @@ --- -name: gldropdown_branches -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57760 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326549 +name: load_balancing_atomic_replica +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49294 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/291193 milestone: '13.11' type: development -group: group::continuous integration -default_enabled: true +group: +default_enabled: false diff --git a/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb b/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb new file mode 100644 index 00000000000..7f6d7ffe9b7 --- /dev/null +++ b/db/post_migrate/20210302150310_schedule_migrate_pages_to_zip_storage.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ScheduleMigratePagesToZipStorage < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'MigratePagesToZipStorage' + BATCH_SIZE = 10 + BATCH_TIME = 5.minutes + + disable_ddl_transaction! + + class ProjectPagesMetadatum < ActiveRecord::Base + extend SuppressCompositePrimaryKeyWarning + + include EachBatch + + self.primary_key = :project_id + self.table_name = 'project_pages_metadata' + self.inheritance_column = :_type_disabled + + scope :deployed, -> { where(deployed: true) } + scope :only_on_legacy_storage, -> { deployed.where(pages_deployment_id: nil) } + end + + def up + queue_background_migration_jobs_by_range_at_intervals( + ProjectPagesMetadatum.only_on_legacy_storage, + MIGRATION, + BATCH_TIME, + batch_size: BATCH_SIZE, + primary_column_name: :project_id + ) + end +end diff --git a/db/schema_migrations/20210302150310 b/db/schema_migrations/20210302150310 new file mode 100644 index 00000000000..251fdb0ba8e --- /dev/null +++ b/db/schema_migrations/20210302150310 @@ -0,0 +1 @@ +7c562d43801c18af48dc526dc6574aebd11689b62bad864b107580d341ba64a1 \ No newline at end of file diff --git a/doc/administration/whats-new.md b/doc/administration/whats-new.md new file mode 100644 index 00000000000..4cbb0b854ae --- /dev/null +++ b/doc/administration/whats-new.md @@ -0,0 +1,29 @@ +--- +stage: Growth +group: Adoption +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# What's new **(FREE)** + +With each monthly release, GitLab includes some of the highlights from the last 10 +GitLab versions in the **What's new** feature. To access it: + +1. In the top navigation bar, select the **{question}** icon. +1. Select **What's new** from the menu. + +The **What's new** describes new features available in multiple +[GitLab tiers](https://about.gitlab.com/pricing). While all users can see the +feature list, the feature list is tailored to your subscription type: + +- Features only available to self-managed installations are not shown on GitLab.com. +- Features only available on GitLab.com are not shown to self-managed installations. + +The **What's new** feature cannot be disabled, but +[is planned](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59011) for a future release. + +## Self-managed installations + +Due to our release post process, the content for **What's new** is not yet finalized +when a new version (`.0` release) is cut. The updated **What's new** is included +in the first patch release, such as `13.10.1`. diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index b14d28d6ec0..ce07bdd2966 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -167,7 +167,8 @@ The following API resources are available outside of project and group contexts | [Sidekiq metrics](sidekiq_metrics.md) **(FREE SELF)** | `/sidekiq` | | [Suggestions](suggestions.md) | `/suggestions` | | [System hooks](system_hooks.md) | `/hooks` | -| [To-dos](todos.md) | `/todos` | +| [To-dos](todos.md) | `/todos` | +| [Usage data](usage_data.md) | `/usage_data` (For GitLab instance [Administrator](../user/permissions.md) users only) | | [Users](users.md) | `/users` | | [Validate `.gitlab-ci.yml` file](lint.md) | `/lint` | | [Version](version.md) | `/version` | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2bef0260565..c803c888449 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -631,7 +631,7 @@ Parsed field from an alert used for custom mappings. | Field | Type | Description | | ----- | ---- | ----------- | | `label` | [`String`](#string) | Human-readable label of the payload path. | -| `path` | [`[String!]`](#string) | Path to value inside payload JSON. | +| `path` | [`[PayloadAlertFieldPathSegment!]`](#payloadalertfieldpathsegment) | Path to value inside payload JSON. | | `type` | [`AlertManagementPayloadAlertFieldType`](#alertmanagementpayloadalertfieldtype) | Type of the parsed value. | ### `AlertManagementPayloadAlertMappingField` @@ -642,7 +642,7 @@ Parsed field (with its name) from an alert used for custom mappings. | ----- | ---- | ----------- | | `fieldName` | [`AlertManagementPayloadAlertFieldName`](#alertmanagementpayloadalertfieldname) | A GitLab alert field name. | | `label` | [`String`](#string) | Human-readable label of the payload path. | -| `path` | [`[String!]`](#string) | Path to value inside payload JSON. | +| `path` | [`[PayloadAlertFieldPathSegment!]`](#payloadalertfieldpathsegment) | Path to value inside payload JSON. | | `type` | [`AlertManagementPayloadAlertFieldType`](#alertmanagementpayloadalertfieldtype) | Type of the parsed value. | ### `AlertManagementPrometheusIntegration` @@ -4827,6 +4827,7 @@ Information about pagination in a connection. | `startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. | | `status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). | | `testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. | +| `testSuite` | [`TestSuite`](#testsuite) | A specific test suite in a pipeline test report. | | `updatedAt` | [`Time!`](#time) | Timestamp of the pipeline's last activity. | | `upstream` | [`Pipeline`](#pipeline) | Pipeline that triggered the pipeline. | | `user` | [`User`](#user) | Pipeline user. | @@ -5276,6 +5277,15 @@ Represents rules that commit pushes must follow. | ----- | ---- | ----------- | | `rejectUnsignedCommits` | [`Boolean!`](#boolean) | Indicates whether commits not signed through GPG will be rejected. | +### `RecentFailures` + +Recent failure history of a test case. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `baseBranch` | [`String`](#string) | Name of the base branch of the project. | +| `count` | [`Int`](#int) | Number of times the test case has failed in the past 14 days. | + ### `Release` Represents a release. @@ -5522,13 +5532,44 @@ Autogenerated return type of RepositionImageDiffNote. | Field | Type | Description | | ----- | ---- | ----------- | -| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. | +| `blobs` | [`RepositoryBlobConnection`](#repositoryblobconnection) | Blobs contained within the repository. | | `branchNames` | [`[String!]`](#string) | Names of branches available in this repository that match the search pattern. | | `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. | | `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. | | `rootRef` | [`String`](#string) | Default branch of the repository. | | `tree` | [`Tree`](#tree) | Tree of the repository. | +### `RepositoryBlob` + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `id` | [`ID!`](#id) | ID of the blob. | +| `lfsOid` | [`String`](#string) | LFS OID of the blob. | +| `mode` | [`String`](#string) | Blob mode. | +| `name` | [`String`](#string) | Blob name. | +| `oid` | [`String!`](#string) | OID of the blob. | +| `path` | [`String!`](#string) | Path of the blob. | +| `webPath` | [`String`](#string) | Web path of the blob. | + +### `RepositoryBlobConnection` + +The connection type for RepositoryBlob. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `edges` | [`[RepositoryBlobEdge]`](#repositoryblobedge) | A list of edges. | +| `nodes` | [`[RepositoryBlob]`](#repositoryblob) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +### `RepositoryBlobEdge` + +An edge in a connection. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`RepositoryBlob`](#repositoryblob) | The item at the end of the edge. | + ### `Requirement` Represents a requirement. @@ -6333,6 +6374,42 @@ An edge in a connection. | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`TerraformStateVersionRegistry`](#terraformstateversionregistry) | The item at the end of the edge. | +### `TestCase` + +Test case in pipeline test report. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `attachmentUrl` | [`String`](#string) | URL of the test case attachment file. | +| `classname` | [`String`](#string) | Classname of the test case. | +| `executionTime` | [`Float`](#float) | Test case execution time in seconds. | +| `file` | [`String`](#string) | Path to the file of the test case. | +| `name` | [`String`](#string) | Name of the test case. | +| `recentFailures` | [`RecentFailures`](#recentfailures) | Recent failure history of the test case on the base branch. | +| `stackTrace` | [`String`](#string) | Stack trace of the test case. | +| `status` | [`TestCaseStatus`](#testcasestatus) | Status of the test case (error, failed, success, skipped). | +| `systemOutput` | [`String`](#string) | System output of the test case. | + +### `TestCaseConnection` + +The connection type for TestCase. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `count` | [`Int!`](#int) | Total count of collection. | +| `edges` | [`[TestCaseEdge]`](#testcaseedge) | A list of edges. | +| `nodes` | [`[TestCase]`](#testcase) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +### `TestCaseEdge` + +An edge in a connection. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`TestCase`](#testcase) | The item at the end of the edge. | + ### `TestReport` Represents a requirement test report. @@ -6386,6 +6463,22 @@ Total test report statistics. | `suiteError` | [`String`](#string) | Test suite error message. | | `time` | [`Float`](#float) | Total duration of the tests. | +### `TestSuite` + +Test suite in a pipeline test report. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `errorCount` | [`Int`](#int) | Total number of test cases that had an error. | +| `failedCount` | [`Int`](#int) | Total number of test cases that failed in the test suite. | +| `name` | [`String`](#string) | Name of the test suite. | +| `skippedCount` | [`Int`](#int) | Total number of test cases that were skipped in the test suite. | +| `successCount` | [`Int`](#int) | Total number of test cases that succeeded in the test suite. | +| `suiteError` | [`String`](#string) | Test suite error message. | +| `testCases` | [`TestCaseConnection`](#testcaseconnection) | Test cases in the test suite. | +| `totalCount` | [`Int`](#int) | Total number of the test cases in the test suite. | +| `totalTime` | [`Float`](#float) | Total duration of the tests in the test suite. | + ### `TestSuiteSummary` Test suite summary in a pipeline test report. @@ -8424,6 +8517,15 @@ Common sort values. | `updated_asc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_ASC`. Deprecated in 13.5. | | `updated_desc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_DESC`. Deprecated in 13.5. | +### `TestCaseStatus` + +| Value | Description | +| ----- | ----------- | +| `error` | Test case that has a status of error. | +| `failed` | Test case that has a status of failed. | +| `skipped` | Test case that has a status of skipped. | +| `success` | Test case that has a status of success. | + ### `TestReportState` State of a test report. @@ -8967,6 +9069,10 @@ A `PackagesPackageID` is a global ID. It is encoded as a string. An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`. +### `PayloadAlertFieldPathSegment` + +String or integer. + ### `ProjectID` A `ProjectID` is a global ID. It is encoded as a string. diff --git a/doc/api/usage_data.md b/doc/api/usage_data.md index 671e60be587..024caa96565 100644 --- a/doc/api/usage_data.md +++ b/doc/api/usage_data.md @@ -5,17 +5,61 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference, api --- -# UsageData API **(FREE SELF)** +# Usage Data API **(FREE SELF)** -The UsageData API, associated with [Usage Ping](../development/usage_ping/index.md), is available only for -the use of GitLab instance [Administrator](../user/permissions.md) users. +The Usage Data API is associated with [Usage Ping](../development/usage_ping/index.md). -## UsageDataQueries API +## Export metric definitions as a single YAML file + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57270) in GitLab 13.11. + +Export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](../development/usage_ping/dictionary.md), for easier importing. + +```plaintext +GET /usage_data/metric_definitions +``` + +Example request: + +```shell +curl "https://gitlab.example.com/api/v4/usage_data/metric_definitions" +``` + +Example response: + +```yaml +--- +- key_path: redis_hll_counters.search.i_search_paid_monthly + description: Calculated unique users to perform a search with a paid license enabled + by month + product_section: enablement + product_stage: enablement + product_group: group::global search + product_category: global_search + value_type: number + status: data_available + time_frame: 28d + data_source: redis_hll + distribution: + - ee + tier: + - premium + - ultimate +... +``` + +## Export Usage Ping SQL queries + +This action is available only for the GitLab instance [Administrator](../user/permissions.md) users. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57016) in GitLab 13.11. > - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default. -Return all of the raw SQL queries used to compute usage ping. +Return all of the raw SQL queries used to compute Usage Ping. + +```plaintext +GET /usage_data/queries +``` Example request: @@ -23,7 +67,7 @@ Example request: curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/usage_data/queries" ``` -Sample response +Example response: ```json { diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md index 4b584c6ecc9..a62b58d6095 100644 --- a/doc/development/usage_ping/index.md +++ b/doc/development/usage_ping/index.md @@ -1411,37 +1411,6 @@ bin/rake gitlab:usage_data:dump_sql_in_json bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml ``` -## Export metric definitions as a single YAML file - -Use this API endpoint to export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](dictionary.md), for easier importing. - -```plaintext -GET /usage_data/metric_definitions -``` - -Response - -```yaml ---- -- key_path: redis_hll_counters.search.i_search_paid_monthly - description: Calculated unique users to perform a search with a paid license enabled - by month - product_section: enablement - product_stage: enablement - product_group: group::global search - product_category: global_search - value_type: number - status: data_available - time_frame: 28d - data_source: redis_hll - distribution: - - ee - tier: - - premium - - ultimate -... -``` - ## Generating and troubleshooting usage ping This activity is to be done via a detached screen session on a remote server. diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index fa0df27585e..667fa9e6b19 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -11,6 +11,8 @@ You can integrate GitLab.com and Jira Cloud using the app in the Atlassian Marketplace. The user configuring GitLab for Jira must have [Maintainer](../../user/permissions.md) permissions in the GitLab namespace. +This integration method supports [smart commits](dvcs.md#smart-commits). + This method is recommended when using GitLab.com and Jira Cloud because data is synchronized in real-time. The DVCS connector updates data only once per hour. If you are not using both of these environments, use the [Jira DVCS Connector](dvcs.md) method. diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index 1d67dab9a84..ce80d370627 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -18,6 +18,25 @@ are accessible. - **Jira Cloud**: Your instance must be accessible through the internet. - **Jira Server**: Your network must allow access to your instance. +## Smart commits + +When connecting GitLab with Jira with DVCS, you can process your Jira issues using +special commands, called +[Smart Commits](https://support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits/), +in your commit messages. With Smart Commits, you can: + +- Comment on issues. +- Record time-tracking information against issues. +- Transition issues to any status defined in the Jira project's workflow. + +Commands must be in the first line of the commit message. The +[Jira Software documentation](https://support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits/) +contains more information about how smart commits work, and what commands are available +for your use. + +For smart commits to work, the committing user on GitLab must have a corresponding +user on Jira with the same email address or username. + ## Configure a GitLab application for DVCS We recommend you create and use a `jira` user in GitLab, and use the account only diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 0ad2972c5a8..31accfdd9e4 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -8,9 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223061) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4. > - [In GitLab 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/300960), KAS became available on GitLab.com under `wss://kas.gitlab.com` through an Early Adopter Program. - -WARNING: -This feature might not be available to you. Check the **version history** note above for details. +> - Introduced in GitLab 13.11, the GitLab Kubernetes Agent became available to every project on GitLab.com. The [GitLab Kubernetes Agent](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent) is an active in-cluster component for solving GitLab and Kubernetes integration @@ -169,17 +167,21 @@ the Agent in subsequent steps. You can create an Agent record with GraphQL: ### Install the Agent into the cluster -Next, install the in-cluster component of the Agent. - -NOTE: -For GitLab.com users, the KAS is available at `wss://kas.gitlab.com`. - -#### One-liner installation - -Replace the value of `agent-token` below with the token received from the previous step. Also, replace `kas-address` with the configured access of the Kubernetes Agent Server: +To install the in-cluster component of the Agent, first you need to define a namespace. To create a new namespace, +for example, `gitlab-kubernetes-agent`, run: ```shell -docker run --pull=always --rm registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/cli:stable generate --agent-token=your-agent-token --kas-address=wss://kas.gitlab.example.com --agent-version stable | kubectl apply -f - +kubectl create namespace gitlab-kubernetes-agent +``` + +To perform a one-liner installation, run the command below. Make sure to replace: + +- `your-agent-token` with the token received from the previous step. +- `gitlab-kubernetes-agent` with the namespace you defined in the previous step. +- `wss://kas.gitlab.example.com` with the configured access of the Kubernetes Agent Server (KAS). For GitLab.com users, the KAS is available under `wss://kas.gitlab.com`. + +```shell +docker run --pull=always --rm registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/cli:stable generate --agent-token=your-agent-token --kas-address=wss://kas.gitlab.example.com --agent-version stable --namespace gitlab-kubernetes-agent | kubectl apply -f - ``` Set `--agent-version` to the latest released patch version matching your @@ -206,17 +208,11 @@ Otherwise, you can follow below for fully manual, detailed installation steps. After generating the token, you must apply it to the Kubernetes cluster. -1. If you haven't previously defined or created a namespace, run the following command: +To create your Secret, run: - ```shell - kubectl create namespace - ``` - -1. Run the following command to create your Secret: - - ```shell - kubectl create secret generic -n gitlab-agent-token --from-literal=token='YOUR_AGENT_TOKEN' - ``` +```shell +kubectl create secret generic -n gitlab-agent-token --from-literal=token='YOUR_AGENT_TOKEN' +``` The following example file contains the Kubernetes resources required for the Agent to be installed. You can modify this diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index dc63a32ed10..56a339e02d2 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1311,7 +1311,6 @@ X-Gitlab-Event: Job Hook "name": "User", "email": "user@gitlab.com", "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon", - "email": "admin@example.com" }, "commit": { "id": 2366, diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md index 58e80504212..3a5a581198b 100644 --- a/doc/user/project/merge_requests/creating_merge_requests.md +++ b/doc/user/project/merge_requests/creating_merge_requests.md @@ -172,6 +172,9 @@ create a merge request from your fork to contribute back to the main project: 1. In the left menu, go to **Merge Requests**, and click **New Merge Request**. 1. In the **Source branch** drop-down list box, select your branch in your forked repository as the source branch. 1. In the **Target branch** drop-down list box, select the branch from the upstream repository as the target branch. + You can set a [default target project](#set-the-default-target-project) to + change the default target branch (which can be useful when working with a + forked project). 1. After entering the credentials, click **Compare branches and continue** to compare your local changes to the upstream repository. 1. Assign a user to review your changes, and click **Submit merge request**. @@ -183,6 +186,24 @@ fork from its upstream project in the **Settings > Advanced Settings** section b For further details, [see the forking workflow documentation](../repository/forking_workflow.md). +## Set the default target project + +Merge requests have a source and a target project which are the same, unless +forking is involved. Creating a fork of the project can cause either of these +scenarios when you create a new merge request: + +- You target an upstream project (the project you forked, and the default + option). +- You target your own fork. + +If you want to have merge requests from a fork by default target your own fork +(instead of the upstream project), you can change the default by: + +1. In your project, go to **Settings > General > Merge requests**. +1. In the **Target project** section, select the option you want to use for + your default target project. +1. Select **Save changes**. + ## New merge request by email **(FREE SELF)** _This feature needs [incoming email](../../../administration/incoming_email.md) diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index bcfd7f5e5d9..6d37d26f6e8 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -214,6 +214,7 @@ Set up your project's merge request settings: - Enable [merge only when all threads are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved). - Enable [`delete source branch after merge` option by default](../merge_requests/getting_started.md#deleting-the-source-branch) - Configure [suggested changes commit messages](../../discussions/index.md#configure-the-commit-message-for-applied-suggestions) +- Configure [the default target project](../merge_requests/creating_merge_requests.md#set-the-default-target-project) for merge requests coming from forks. ### Service Desk diff --git a/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb new file mode 100644 index 00000000000..b7a912da060 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # migrates pages from legacy storage to zip format + # we intentionally use application code here because + # it has a lot of dependencies including models, carrierwave uploaders and service objects + # and copying all or part of this code in the background migration doesn't add much value + # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54578 for discussion + class MigratePagesToZipStorage + def perform(start_id, stop_id) + ::Pages::MigrateFromLegacyStorageService.new(Gitlab::AppLogger, + ignore_invalid_entries: false, + mark_projects_as_not_deployed: false) + .execute_for_batch(start_id..stop_id) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 46ecb10ea2b..c3c1728602c 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -84,7 +84,7 @@ module Gitlab end def metrics - @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics end def observe_creation_duration(duration) @@ -97,6 +97,11 @@ module Gitlab .observe({ source: pipeline.source.to_s }, pipeline.total_size) end + def increment_pipeline_failure_reason_counter(reason) + metrics.pipeline_failure_reason_counter + .increment(reason: (reason || :unknown_failure).to_s) + end + def dangling_build? %i[ondemand_dast_scan webide].include?(source) end diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb index f995f62f87b..22a7dbe61aa 100644 --- a/lib/gitlab/ci/pipeline/chain/helpers.rb +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -13,16 +13,7 @@ module Gitlab pipeline.add_error_message(message) - if drop_reason && persist_pipeline? - if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml) - # Project iid must be called outside a transaction, so we ensure it is set here - # otherwise it may be set within the state transition transaction of the drop! call - # which it will lock the InternalId row for the whole transaction - pipeline.ensure_project_iid! - end - - pipeline.drop!(drop_reason) - end + drop_pipeline!(drop_reason) # TODO: consider not to rely on AR errors directly as they can be # polluted with other unrelated errors (e.g. state machine) @@ -34,8 +25,23 @@ module Gitlab pipeline.add_warning_message(message) end - def persist_pipeline? - command.save_incompleted && !pipeline.readonly? + private + + def drop_pipeline!(drop_reason) + return if pipeline.readonly? + + if drop_reason && command.save_incompleted + if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml) + # Project iid must be called outside a transaction, so we ensure it is set here + # otherwise it may be set within the state transition transaction of the drop! call + # which it will lock the InternalId row for the whole transaction + pipeline.ensure_project_iid! + end + + pipeline.drop!(drop_reason) + else + command.increment_pipeline_failure_reason_counter(drop_reason) + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb index 0d7449813b4..b17ae77d445 100644 --- a/lib/gitlab/ci/pipeline/chain/metrics.rb +++ b/lib/gitlab/ci/pipeline/chain/metrics.rb @@ -14,7 +14,7 @@ module Gitlab end def counter - ::Gitlab::Ci::Pipeline::Metrics.new.pipelines_created_counter + ::Gitlab::Ci::Pipeline::Metrics.pipelines_created_counter end end end diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb index c77f4dcca5a..6cb6fd3920d 100644 --- a/lib/gitlab/ci/pipeline/metrics.rb +++ b/lib/gitlab/ci/pipeline/metrics.rb @@ -4,55 +4,57 @@ module Gitlab module Ci module Pipeline class Metrics - include Gitlab::Utils::StrongMemoize + def self.pipeline_creation_duration_histogram + name = :gitlab_ci_pipeline_creation_duration_seconds + comment = 'Pipeline creation duration' + labels = {} + buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0] - def pipeline_creation_duration_histogram - strong_memoize(:pipeline_creation_duration_histogram) do - name = :gitlab_ci_pipeline_creation_duration_seconds - comment = 'Pipeline creation duration' - labels = {} - buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0] - - ::Gitlab::Metrics.histogram(name, comment, labels, buckets) - end + ::Gitlab::Metrics.histogram(name, comment, labels, buckets) end - def pipeline_size_histogram - strong_memoize(:pipeline_size_histogram) do - name = :gitlab_ci_pipeline_size_builds - comment = 'Pipeline size' - labels = { source: nil } - buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000] + def self.pipeline_size_histogram + name = :gitlab_ci_pipeline_size_builds + comment = 'Pipeline size' + labels = { source: nil } + buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000] - ::Gitlab::Metrics.histogram(name, comment, labels, buckets) - end + ::Gitlab::Metrics.histogram(name, comment, labels, buckets) end - def pipeline_processing_events_counter - strong_memoize(:pipeline_processing_events_counter) do - name = :gitlab_ci_pipeline_processing_events_total - comment = 'Total amount of pipeline processing events' + def self.pipeline_processing_events_counter + name = :gitlab_ci_pipeline_processing_events_total + comment = 'Total amount of pipeline processing events' - Gitlab::Metrics.counter(name, comment) - end + Gitlab::Metrics.counter(name, comment) end - def pipelines_created_counter - strong_memoize(:pipelines_created_count) do - name = :pipelines_created_total - comment = 'Counter of pipelines created' + def self.pipelines_created_counter + name = :pipelines_created_total + comment = 'Counter of pipelines created' - Gitlab::Metrics.counter(name, comment) - end + Gitlab::Metrics.counter(name, comment) end - def legacy_update_jobs_counter - strong_memoize(:legacy_update_jobs_counter) do - name = :ci_legacy_update_jobs_as_retried_total - comment = 'Counter of occurrences when jobs were not being set as retried before update_retried' + def self.legacy_update_jobs_counter + name = :ci_legacy_update_jobs_as_retried_total + comment = 'Counter of occurrences when jobs were not being set as retried before update_retried' - Gitlab::Metrics.counter(name, comment) - end + Gitlab::Metrics.counter(name, comment) + end + + def self.pipeline_failure_reason_counter + name = :gitlab_ci_pipeline_failure_reasons + comment = 'Counter of pipeline failure reasons' + + Gitlab::Metrics.counter(name, comment) + end + + def self.job_failure_reason_counter + name = :gitlab_ci_job_failure_reasons + comment = 'Counter of job failure reasons' + + Gitlab::Metrics.counter(name, comment) end end end diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb index 324695adb1f..4aa33ed7946 100644 --- a/lib/gitlab/database/background_migration/batched_migration.rb +++ b/lib/gitlab/database/background_migration/batched_migration.rb @@ -57,6 +57,13 @@ module Gitlab def batch_class_name=(class_name) write_attribute(:batch_class_name, class_name.demodulize) end + + def prometheus_labels + @prometheus_labels ||= { + migration_id: id, + migration_identifier: "%s/%s.%s" % [job_class_name, table_name, column_name] + } + end end end end diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb index bbaa8040203..c276f8ce75b 100644 --- a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb +++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb @@ -4,6 +4,8 @@ module Gitlab module Database module BackgroundMigration class BatchedMigrationWrapper + extend Gitlab::Utils::StrongMemoize + # Wraps the execution of a batched_background_migration. # # Updates the job's tracking records with the status of the migration @@ -23,6 +25,7 @@ module Gitlab raise e ensure finish_tracking_execution(batch_tracking_record) + track_prometheus_metrics(batch_tracking_record) end private @@ -51,6 +54,65 @@ module Gitlab tracking_record.finished_at = Time.current tracking_record.save! end + + def track_prometheus_metrics(tracking_record) + migration = tracking_record.batched_migration + base_labels = migration.prometheus_labels + + metric_for(:gauge_batch_size).set(base_labels, tracking_record.batch_size) + metric_for(:gauge_sub_batch_size).set(base_labels, tracking_record.sub_batch_size) + metric_for(:counter_updated_tuples).increment(base_labels, tracking_record.batch_size) + + # Time efficiency: Ratio of duration to interval (ideal: less than, but close to 1) + efficiency = (tracking_record.finished_at - tracking_record.started_at).to_i / migration.interval.to_f + metric_for(:histogram_time_efficiency).observe(base_labels, efficiency) + + if metrics = tracking_record.metrics + metrics['timings']&.each do |key, timings| + summary = metric_for(:histogram_timings) + labels = base_labels.merge(operation: key) + + timings.each do |timing| + summary.observe(labels, timing) + end + end + end + end + + def metric_for(name) + self.class.metrics[name] + end + + def self.metrics + strong_memoize(:metrics) do + { + gauge_batch_size: Gitlab::Metrics.gauge( + :batched_migration_job_batch_size, + 'Batch size for a batched migration job' + ), + gauge_sub_batch_size: Gitlab::Metrics.gauge( + :batched_migration_job_sub_batch_size, + 'Sub-batch size for a batched migration job' + ), + counter_updated_tuples: Gitlab::Metrics.counter( + :batched_migration_job_updated_tuples_total, + 'Number of tuples updated by batched migration job' + ), + histogram_timings: Gitlab::Metrics.histogram( + :batched_migration_job_duration_seconds, + 'Timings for a batched migration job', + {}, + [0.1, 0.25, 0.5, 1, 5].freeze + ), + histogram_time_efficiency: Gitlab::Metrics.histogram( + :batched_migration_job_time_efficiency, + 'Ratio of job duration to interval', + {}, + [0.5, 0.9, 1, 1.5, 2].freeze + ) + } + end + end end end end diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb index 3dda68bf93f..a1fabe75a97 100644 --- a/lib/gitlab/metrics/background_transaction.rb +++ b/lib/gitlab/metrics/background_transaction.rb @@ -34,8 +34,9 @@ module Gitlab def labels @labels ||= { - endpoint_id: current_context&.get_attribute(:caller_id), - feature_category: current_context&.get_attribute(:feature_category) + endpoint_id: endpoint_id, + feature_category: feature_category, + queue: queue } end @@ -44,6 +45,21 @@ module Gitlab def current_context Labkit::Context.current end + + def feature_category + current_context&.get_attribute(:feature_category) + end + + def endpoint_id + current_context&.get_attribute(:caller_id) + end + + def queue + worker_class = endpoint_id.to_s.safe_constantize + return if worker_class.blank? || !worker_class.respond_to?(:queue) + + worker_class.queue.to_s + end end end end diff --git a/lib/gitlab/pages/migration_helper.rb b/lib/gitlab/pages/migration_helper.rb new file mode 100644 index 00000000000..8f8667fafd9 --- /dev/null +++ b/lib/gitlab/pages/migration_helper.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Gitlab + module Pages + class MigrationHelper + def initialize(logger = nil) + @logger = logger + end + + def migrate_to_remote_storage + deployments = ::PagesDeployment.with_files_stored_locally + migrate(deployments, ObjectStorage::Store::REMOTE) + end + + def migrate_to_local_storage + deployments = ::PagesDeployment.with_files_stored_remotely + migrate(deployments, ObjectStorage::Store::LOCAL) + end + + private + + def batch_size + ENV.fetch('MIGRATION_BATCH_SIZE', 10).to_i + end + + def migrate(deployments, store) + deployments.find_each(batch_size: batch_size) do |deployment| # rubocop:disable CodeReuse/ActiveRecord + deployment.file.migrate!(store) + + log_success(deployment, store) + rescue => e + log_error(e, deployment) + end + end + + def log_success(deployment, store) + logger.info("Transferred deployment ID #{deployment.id} of type #{deployment.file_type} with size #{deployment.size} to #{storage_label(store)} storage") + end + + def log_error(err, deployment) + logger.warn("Failed to transfer deployment of type #{deployment.file_type} and ID #{deployment.id} with error: #{err.message}") + end + + def storage_label(store) + if store == ObjectStorage::Store::LOCAL + 'local' + else + 'object' + end + end + end + end +end diff --git a/lib/gitlab/usage_data_non_sql_metrics.rb b/lib/gitlab/usage_data_non_sql_metrics.rb index 6bd74a2a993..1f72bf4ce26 100644 --- a/lib/gitlab/usage_data_non_sql_metrics.rb +++ b/lib/gitlab/usage_data_non_sql_metrics.rb @@ -24,6 +24,12 @@ module Gitlab def histogram(relation, column, buckets:, bucket_size: buckets.size) SQL_METRIC_DEFAULT end + + def maximum_id(model) + end + + def minimum_id(model) + end end end end diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake index 606d1369e18..ee2931f0c4f 100644 --- a/lib/tasks/gitlab/pages.rake +++ b/lib/tasks/gitlab/pages.rake @@ -9,10 +9,9 @@ namespace :gitlab do logger.info('Starting to migrate legacy pages storage to zip deployments') result = ::Pages::MigrateFromLegacyStorageService.new(logger, - migration_threads: migration_threads, - batch_size: batch_size, ignore_invalid_entries: ignore_invalid_entries, - mark_projects_as_not_deployed: mark_projects_as_not_deployed).execute + mark_projects_as_not_deployed: mark_projects_as_not_deployed) + .execute_with_threads(threads: migration_threads, batch_size: batch_size) logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.") logger.info("- The #{result[:migrated]} projects migrated successfully") @@ -58,5 +57,33 @@ namespace :gitlab do ENV.fetch('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'false') ) end + + namespace :deployments do + task migrate_to_object_storage: :gitlab_environment do + logger = Logger.new(STDOUT) + logger.info('Starting transfer of pages deployments to remote storage') + + helper = Gitlab::Pages::MigrationHelper.new(logger) + + begin + helper.migrate_to_remote_storage + rescue => e + logger.error(e.message) + end + end + + task migrate_to_local: :gitlab_environment do + logger = Logger.new(STDOUT) + logger.info('Starting transfer of Pages deployments to local storage') + + helper = Gitlab::Pages::MigrationHelper.new(logger) + + begin + helper.migrate_to_local_storage + rescue => e + logger.error(e.message) + end + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 89890deb77a..58ca3603ff0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5319,9 +5319,6 @@ msgstr "" msgid "Branches|Show stale branches" msgstr "" -msgid "Branches|Sort by" -msgstr "" - msgid "Branches|Stale" msgstr "" @@ -6656,9 +6653,6 @@ msgstr "" msgid "CloudLicense|Paste your activation code" msgstr "" -msgid "CloudLicense|This instance is currently using the %{planName} plan." -msgstr "" - msgid "CloudLicense|This is the highest peak of users on your installation since the license started." msgstr "" @@ -6677,9 +6671,6 @@ msgstr "" msgid "CloudLicense|You'll be charged for %{trueUpLinkStart}users over license%{trueUpLinkEnd} on a quarterly or annual basis, depending on the terms of your agreement." msgstr "" -msgid "CloudLicense|Your subscription" -msgstr "" - msgid "Cluster" msgstr "" @@ -30378,6 +30369,12 @@ msgstr "" msgid "SuperSonics|Valid From" msgstr "" +msgid "SuperSonics|You do not have an active subscription" +msgstr "" + +msgid "SuperSonics|Your subscription" +msgstr "" + msgid "Support" msgstr "" diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index d9fae3db23f..cb7323ac62d 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -14,7 +14,6 @@ module QA include SubMenus::Packages view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do - element :activity_link element :merge_requests_link element :snippets_link element :members_link @@ -24,6 +23,10 @@ module QA element :wiki_link end + view 'app/views/shared/nav/_sidebar_menu_item.html.haml' do + element :sidebar_menu_item_link + end + def click_merge_requests within_sidebar do click_element(:merge_requests_link) @@ -38,7 +41,7 @@ module QA def click_activity within_sidebar do - click_element(:activity_link) + click_element(:sidebar_menu_item_link, menu_item: 'Activity') end end diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb index 8a648279919..ecb3148b486 100644 --- a/qa/qa/page/project/sub_menus/project.rb +++ b/qa/qa/page/project/sub_menus/project.rb @@ -13,8 +13,8 @@ module QA base.class_eval do include QA::Page::Project::SubMenus::Common - view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do - element :project_link + view 'app/views/shared/nav/_sidebar_menu.html.haml' do + element :sidebar_menu_link end end end @@ -22,7 +22,7 @@ module QA def click_project retry_on_exception do within_sidebar do - click_element(:project_link) + click_element(:sidebar_menu_link, menu_item: 'Project overview') end end end diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb index 3da72bc1f24..bebb4bb679b 100644 --- a/spec/features/projects/branches/user_deletes_branch_spec.rb +++ b/spec/features/projects/branches/user_deletes_branch_spec.rb @@ -12,10 +12,12 @@ RSpec.describe "User deletes branch", :js do end it "deletes branch" do - stub_feature_flags(gldropdown_branches: false) visit(project_branches_path(project)) - fill_in("branch-search", with: "improve/awesome").native.send_keys(:enter) + branch_search = find('input[data-testid="branch-search"]') + + branch_search.set('improve/awesome') + branch_search.native.send_keys(:enter) page.within(".js-branch-improve\\/awesome") do accept_alert { find(".btn-danger").click } @@ -25,23 +27,4 @@ RSpec.describe "User deletes branch", :js do expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden) end - - context 'with gldropdown_branches enabled' do - it "deletes branch" do - visit(project_branches_path(project)) - - branch_search = find('input[data-testid="branch-search"]') - - branch_search.set('improve/awesome') - branch_search.native.send_keys(:enter) - - page.within(".js-branch-improve\\/awesome") do - accept_alert { find(".btn-danger").click } - end - - wait_for_requests - - expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden) - end - end end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 98c125ec5db..f805416b03d 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -86,29 +86,16 @@ RSpec.describe 'Branches' do describe 'Find branches' do it 'shows filtered branches', :js do - stub_feature_flags(gldropdown_branches: false) visit project_branches_path(project) - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) + branch_search = find('input[data-testid="branch-search"]') + + branch_search.set('fix') + branch_search.native.send_keys(:enter) expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) end - - context 'with gldropdown_branches enabled' do - it 'shows filtered branches', :js do - visit project_branches_path(project) - - branch_search = find('input[data-testid="branch-search"]') - - branch_search.set('fix') - branch_search.native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - end - end end describe 'Delete unprotected branch on Overview' do @@ -129,52 +116,28 @@ RSpec.describe 'Branches' do expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc)) end - it 'sorts the branches by name' do - stub_feature_flags(gldropdown_branches: false) + it 'sorts the branches by name', :js do visit project_branches_filtered_path(project, state: 'all') click_button "Last updated" # Open sorting dropdown - click_link "Name" + within '[data-testid="branches-dropdown"]' do + find('p', text: 'Name').click + end expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name)) end - context 'with gldropdown_branches enabled' do - it 'sorts the branches by name', :js do - visit project_branches_filtered_path(project, state: 'all') - - click_button "Last updated" # Open sorting dropdown - within '[data-testid="branches-dropdown"]' do - find('p', text: 'Name').click - end - - expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name)) - end - end - - it 'sorts the branches by oldest updated' do - stub_feature_flags(gldropdown_branches: false) + it 'sorts the branches by oldest updated', :js do visit project_branches_filtered_path(project, state: 'all') click_button "Last updated" # Open sorting dropdown - click_link "Oldest updated" + within '[data-testid="branches-dropdown"]' do + find('p', text: 'Oldest updated').click + end expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc)) end - context 'with gldropdown_branches enabled' do - it 'sorts the branches by oldest updated', :js do - visit project_branches_filtered_path(project, state: 'all') - - click_button "Last updated" # Open sorting dropdown - within '[data-testid="branches-dropdown"]' do - find('p', text: 'Oldest updated').click - end - - expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc)) - end - end - it 'avoids a N+1 query in branches index' do control_count = ActiveRecord::QueryRecorder.new { visit project_branches_path(project) }.count @@ -186,39 +149,26 @@ RSpec.describe 'Branches' do describe 'Find branches on All branches' do it 'shows filtered branches', :js do - stub_feature_flags(gldropdown_branches: false) visit project_branches_filtered_path(project, state: 'all') - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) + branch_search = find('input[data-testid="branch-search"]') + + branch_search.set('fix') + branch_search.native.send_keys(:enter) expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) end - - context 'with gldropdown_branches enabled' do - it 'shows filtered branches', :js do - visit project_branches_filtered_path(project, state: 'all') - - branch_search = find('input[data-testid="branch-search"]') - - branch_search.set('fix') - branch_search.native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - end - end end describe 'Delete unprotected branch on All branches' do it 'removes branch after confirmation', :js do - stub_feature_flags(gldropdown_branches: false) visit project_branches_filtered_path(project, state: 'all') - fill_in 'branch-search', with: 'fix' + branch_search = find('input[data-testid="branch-search"]') - find('#branch-search').native.send_keys(:enter) + branch_search.set('fix') + branch_search.native.send_keys(:enter) expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) @@ -227,24 +177,6 @@ RSpec.describe 'Branches' do expect(page).not_to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 0) end - - context 'with gldropdown_branches enabled' do - it 'removes branch after confirmation', :js do - visit project_branches_filtered_path(project, state: 'all') - - branch_search = find('input[data-testid="branch-search"]') - - branch_search.set('fix') - branch_search.native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - accept_confirm { find('.js-branch-fix .btn-danger').click } - - expect(page).not_to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 0) - end - end end context 'on project with 0 branch' do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index a88dc9f8655..207b74c990a 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -22,25 +22,13 @@ RSpec.describe 'Protected Branches', :js do end it 'does not allow developer to removes protected branch' do - stub_feature_flags(gldropdown_branches: false) visit project_branches_path(project) - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) + find('input[data-testid="branch-search"]').set('fix') + find('input[data-testid="branch-search"]').native.send_keys(:enter) expect(page).to have_css('.btn-danger.disabled') end - - context 'with gldropdown_branches enabled' do - it 'does not allow developer to removes protected branch' 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_css('.btn-danger.disabled') - end - end end end @@ -57,11 +45,10 @@ RSpec.describe 'Protected Branches', :js do end it 'removes branch after modal confirmation' do - stub_feature_flags(gldropdown_branches: false) visit project_branches_path(project) - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) + 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) @@ -71,33 +58,11 @@ RSpec.describe 'Protected Branches', :js do fill_in 'delete_branch_input', with: 'fix' click_link 'Delete protected branch' - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) + find('input[data-testid="branch-search"]').set('fix') + find('input[data-testid="branch-search"]').native.send_keys(:enter) expect(page).to have_content('No branches to show') end - - context 'with gldropdown_branches enabled' do - 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) - page.find('[data-target="#modal-delete-branch"]').click - - expect(page).to have_css('.js-delete-branch[disabled]') - fill_in 'delete_branch_input', with: 'fix' - click_link 'Delete protected branch' - - find('input[data-testid="branch-search"]').set('fix') - find('input[data-testid="branch-search"]').native.send_keys(:enter) - - expect(page).to have_content('No branches to show') - end - end end end diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb index 7c5625486f5..55b96361f03 100644 --- a/spec/features/whats_new_spec.rb +++ b/spec/features/whats_new_spec.rb @@ -2,34 +2,60 @@ require "spec_helper" -RSpec.describe "renders a `whats new` dropdown item", :js do +RSpec.describe "renders a `whats new` dropdown item" do let_it_be(:user) { create(:user) } - before do - sign_in(user) - end + context 'when not logged in' do + it 'and on .com it renders' do + allow(Gitlab).to receive(:com?).and_return(true) - it 'shows notification dot and count and removes it once viewed' do - visit root_dashboard_path + visit user_path(user) - page.within '.header-help' do - expect(page).to have_selector('.notification-dot', visible: true) + page.within '.header-help' do + find('.header-help-dropdown-toggle').click - find('.header-help-dropdown-toggle').click - - expect(page).to have_button(text: "What's new") - expect(page).to have_selector('.js-whats-new-notification-count') - - find('button', text: "What's new").click + expect(page).to have_button(text: "What's new") + end end - find('.whats-new-drawer .gl-drawer-close-button').click - find('.header-help-dropdown-toggle').click + it "doesn't render what's new" do + visit user_path(user) - page.within '.header-help' do - expect(page).not_to have_selector('.notification-dot', visible: true) - expect(page).to have_button(text: "What's new") - expect(page).not_to have_selector('.js-whats-new-notification-count') + page.within '.header-help' do + find('.header-help-dropdown-toggle').click + + expect(page).not_to have_button(text: "What's new") + end + end + end + + context 'when logged in', :js do + before do + sign_in(user) + end + + it 'shows notification dot and count and removes it once viewed' do + visit root_dashboard_path + + page.within '.header-help' do + expect(page).to have_selector('.notification-dot', visible: true) + + find('.header-help-dropdown-toggle').click + + expect(page).to have_button(text: "What's new") + expect(page).to have_selector('.js-whats-new-notification-count') + + find('button', text: "What's new").click + end + + find('.whats-new-drawer .gl-drawer-close-button').click + find('.header-help-dropdown-toggle').click + + page.within '.header-help' do + expect(page).not_to have_selector('.notification-dot', visible: true) + expect(page).to have_button(text: "What's new") + expect(page).not_to have_selector('.js-whats-new-notification-count') + end end end end diff --git a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb new file mode 100644 index 00000000000..606c6eb03a3 --- /dev/null +++ b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::TestSuiteResolver do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + + describe '#resolve' do + subject(:test_suite) { resolve(described_class, obj: pipeline, args: { build_ids: build_ids }) } + + context 'when pipeline has builds with test reports' do + let_it_be(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') } + + let(:suite_name) { 'test' } + let(:build_ids) { pipeline.latest_builds.pluck(:id) } + + before do + build = main_pipeline.builds.last + build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window + + # The JUnit fixture for the given build has 3 failures. + # This service will create 1 test case failure record for each. + Ci::TestFailureHistoryService.new(main_pipeline).execute + end + + it 'renders test suite data' do + expect(test_suite[:name]).to eq('test') + + # Each test failure in this pipeline has a matching failure in the default branch + recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] } + expect(recent_failures).to eq([ + { count: 1, base_branch: 'master' }, + { count: 1, base_branch: 'master' }, + { count: 1, base_branch: 'master' } + ]) + end + end + + context 'when pipeline has no builds that matches the given build_ids' do + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + + let(:suite_name) { 'test' } + let(:build_ids) { [non_existing_record_id] } + + it 'returns nil' do + expect(test_suite).to be_nil + end + end + end +end diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index 56bd1294d5c..c7d2cbdb765 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Types::Ci::PipelineType do coverage created_at updated_at started_at finished_at committed_at stages user retryable cancelable jobs source_job job downstream upstream path project active user_permissions warnings commit_path uses_needs - test_report_summary + test_report_summary test_suite ] if Gitlab.ee? diff --git a/spec/graphql/types/ci/recent_failures_type_spec.rb b/spec/graphql/types/ci/recent_failures_type_spec.rb new file mode 100644 index 00000000000..38369da46bf --- /dev/null +++ b/spec/graphql/types/ci/recent_failures_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RecentFailuresType do + specify { expect(described_class.graphql_name).to eq('RecentFailures') } + + it 'contains attributes related to a recent failure history for a test case' do + expected_fields = %w[ + count base_branch + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_case_status_enum_spec.rb b/spec/graphql/types/ci/test_case_status_enum_spec.rb new file mode 100644 index 00000000000..ba2d1aefb20 --- /dev/null +++ b/spec/graphql/types/ci/test_case_status_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestCaseStatusEnum do + specify { expect(described_class.graphql_name).to eq('TestCaseStatus') } + + it 'exposes all test case status types' do + expect(described_class.values.keys).to eq( + ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES + ) + end +end diff --git a/spec/graphql/types/ci/test_case_type_spec.rb b/spec/graphql/types/ci/test_case_type_spec.rb new file mode 100644 index 00000000000..e6cd70c287e --- /dev/null +++ b/spec/graphql/types/ci/test_case_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestCaseType do + specify { expect(described_class.graphql_name).to eq('TestCase') } + + it 'contains attributes related to a pipeline test case' do + expected_fields = %w[ + name status classname file attachment_url execution_time stack_trace system_output recent_failures + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/test_suite_type_spec.rb b/spec/graphql/types/ci/test_suite_type_spec.rb new file mode 100644 index 00000000000..d9caca3e2c3 --- /dev/null +++ b/spec/graphql/types/ci/test_suite_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::TestSuiteType do + specify { expect(described_class.graphql_name).to eq('TestSuite') } + + it 'contains attributes related to a pipeline test suite' do + expected_fields = %w[ + name total_time total_count success_count failed_count skipped_count error_count suite_error test_cases + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb new file mode 100644 index 00000000000..f8647e4e964 --- /dev/null +++ b/spec/graphql/types/repository/blob_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Repository::BlobType do + specify { expect(described_class.graphql_name).to eq('RepositoryBlob') } + + specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) } +end diff --git a/spec/helpers/branches_helper_spec.rb b/spec/helpers/branches_helper_spec.rb index 79f98550b3d..2ad15adff59 100644 --- a/spec/helpers/branches_helper_spec.rb +++ b/spec/helpers/branches_helper_spec.rb @@ -47,19 +47,4 @@ RSpec.describe BranchesHelper do end end end - - describe '#gl_dropdown_branches_enabled?' do - context 'when the feature is enabled' do - it 'returns true' do - expect(helper.gldropdrown_branches_enabled?).to be_truthy - end - end - - context 'when the feature is disabled' do - it 'returns false' do - stub_feature_flags(gldropdown_branches: false) - expect(helper.gldropdrown_branches_enabled?).to be_falsy - end - end - end end diff --git a/spec/helpers/whats_new_helper_spec.rb b/spec/helpers/whats_new_helper_spec.rb index f7f0d19db30..0e4b4621560 100644 --- a/spec/helpers/whats_new_helper_spec.rb +++ b/spec/helpers/whats_new_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe WhatsNewHelper do + include Devise::Test::ControllerHelpers + describe '#whats_new_version_digest' do let(:digest) { 'digest' } @@ -32,4 +34,30 @@ RSpec.describe WhatsNewHelper do end end end + + describe '#display_whats_new?' do + subject { helper.display_whats_new? } + + it 'returns true when gitlab.com' do + allow(Gitlab).to receive(:dev_env_org_or_com?).and_return(true) + + expect(subject).to be true + end + + context 'when self-managed' do + before do + allow(Gitlab).to receive(:dev_env_org_or_com?).and_return(false) + end + + it 'returns true if user is signed in' do + sign_in(create(:user)) + + expect(subject).to be true + end + + it "returns false if user isn't signed in" do + expect(subject).to be false + end + end + end end diff --git a/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb new file mode 100644 index 00000000000..557dd8ddee6 --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_pages_to_zip_storage_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::MigratePagesToZipStorage do + let(:namespace) { create(:group) } # rubocop: disable RSpec/FactoriesInMigrationSpecs + let(:migration) { described_class.new } + + describe '#perform' do + context 'when there is project to migrate' do + let!(:project) { create_project('project') } + + after do + FileUtils.rm_rf(project.pages_path) + end + + it 'migrates project to zip storage' do + expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, + anything, + ignore_invalid_entries: false, + mark_projects_as_not_deployed: false) do |service| + expect(service).to receive(:execute_for_batch).with(project.id..project.id).and_call_original + end + + migration.perform(project.id, project.id) + + expect(project.reload.pages_metadatum.pages_deployment.file.filename).to eq("_migrated.zip") + end + end + end + + def create_project(path) + project = create(:project) # rubocop: disable RSpec/FactoriesInMigrationSpecs + project.mark_pages_as_deployed + + FileUtils.mkdir_p File.join(project.pages_path, "public") + File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| + f.write("Hello!") + end + + project + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 9ca5aeeea58..900dfec38e2 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -321,4 +321,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do it { is_expected.to be_falsey } end end + + describe '#increment_pipeline_failure_reason_counter' do + let(:command) { described_class.new } + let(:reason) { :size_limit_exceeded } + + subject { command.increment_pipeline_failure_reason_counter(reason) } + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: reason.to_s) }.by(1) + end + + context 'when the reason is nil' do + let(:reason) { nil } + + it 'increments the error metric with unknown_failure' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb index 78363be7f36..23cdec61bb3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -11,7 +11,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do let(:save_incompleted) { false } let(:command) do - double(:command, + Gitlab::Ci::Pipeline::Chain::Command.new( project: project, pipeline_seed: pipeline_seed, save_incompleted: save_incompleted @@ -49,6 +49,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.deployments_limit_exceeded?).to be true end + + it 'calls increment_pipeline_failure_reason_counter' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { perform }.to change { counter.get(reason: 'deployments_limit_exceeded') }.by(1) + end end context 'when not saving incomplete pipelines' do @@ -71,6 +76,12 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.']) end + + it 'increments the error metric' do + expect(command).to receive(:increment_pipeline_failure_reason_counter).with(:deployments_limit_exceeded) + + perform + end end it 'logs the error' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 53d8ca740b5..62de4d2e96d 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -96,6 +96,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do it 'wastes pipeline iid' do expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 end + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { run_chain }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end end describe 'pipeline protect' do diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb index a7f29da7962..261e23d0745 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb @@ -195,4 +195,17 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m describe '#batch_class_name=' do it_behaves_like 'an attr_writer that demodulizes assigned class names', :batch_class_name end + + describe '#prometheus_labels' do + let(:batched_migration) { create(:batched_background_migration, job_class_name: 'TestMigration', table_name: 'foo', column_name: 'bar') } + + it 'returns a hash with labels for the migration' do + labels = { + migration_id: batched_migration.id, + migration_identifier: 'TestMigration/foo.bar' + } + + expect(batched_migration.prometheus_labels).to eq(labels) + end + end end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index e60f33adec8..00d13f23d36 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '#perform' do - let(:migration_wrapper) { described_class.new } + subject { described_class.new.perform(job_record) } + let(:job_class) { Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob } let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) } @@ -18,7 +19,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' it 'runs the migration job' do expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') - migration_wrapper.perform(job_record) + subject end it 'updates the tracking record in the database' do @@ -30,7 +31,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' expect(job_record).to receive(:update!).with(hash_including(attempts: 1, status: :running)).and_call_original freeze_time do - migration_wrapper.perform(job_record) + subject reloaded_job_record = job_record.reload @@ -41,12 +42,66 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' end end + context 'reporting prometheus metrics' do + let(:labels) { job_record.batched_migration.prometheus_labels } + + before do + allow(job_instance).to receive(:perform) + end + + it 'reports batch_size' do + expect(described_class.metrics[:gauge_batch_size]).to receive(:set).with(labels, job_record.batch_size) + + subject + end + + it 'reports sub_batch_size' do + expect(described_class.metrics[:gauge_sub_batch_size]).to receive(:set).with(labels, job_record.sub_batch_size) + + subject + end + + it 'reports updated tuples (currently based on batch_size)' do + expect(described_class.metrics[:counter_updated_tuples]).to receive(:increment).with(labels, job_record.batch_size) + + subject + end + + it 'reports summary of query timings' do + metrics = { 'timings' => { 'update_all' => [1, 2, 3, 4, 5] } } + + expect(job_instance).to receive(:batch_metrics).and_return(metrics) + + metrics['timings'].each do |key, timings| + summary_labels = labels.merge(operation: key) + timings.each do |timing| + expect(described_class.metrics[:histogram_timings]).to receive(:observe).with(summary_labels, timing) + end + end + + subject + end + + it 'reports time efficiency' do + freeze_time do + expect(Time).to receive(:current).and_return(Time.zone.now - 5.seconds).ordered + expect(Time).to receive(:current).and_return(Time.zone.now).ordered + + ratio = 5 / job_record.batched_migration.interval.to_f + + expect(described_class.metrics[:histogram_time_efficiency]).to receive(:observe).with(labels, ratio) + + subject + end + end + end + context 'when the migration job does not raise an error' do it 'marks the tracking record as succeeded' do expect(job_instance).to receive(:perform).with(1, 10, 'events', 'id', 1, 'id', 'other_id') freeze_time do - migration_wrapper.perform(job_record) + subject reloaded_job_record = job_record.reload @@ -63,7 +118,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' .and_raise(RuntimeError, 'Something broke!') freeze_time do - expect { migration_wrapper.perform(job_record) }.to raise_error(RuntimeError, 'Something broke!') + expect { subject }.to raise_error(RuntimeError, 'Something broke!') reloaded_job_record = job_record.reload diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb index 15e52f7b6c8..d36ee24fc50 100644 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb @@ -29,17 +29,60 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do end describe '#labels' do - it 'provides labels with endpoint_id and feature_category' do - Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do - expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects' }) + context 'when the worker queue is accessible' do + before do + test_worker_class = Class.new do + def self.queue + 'test_worker' + end + end + stub_const('TestWorker', test_worker_class) + end + + it 'provides labels with endpoint_id, feature_category and queue' do + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' }) + end + end + end + + context 'when the worker name does not exist' do + it 'provides labels with endpoint_id and feature_category' do + # 123TestWorker is an invalid constant + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: '123TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: '123TestWorker', feature_category: 'projects', queue: nil }) + end + end + end + + context 'when the worker queue is not accessible' do + before do + stub_const('TestWorker', Class.new) + end + + it 'provides labels with endpoint_id and feature_category' do + Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do + expect(transaction.labels).to eq({ endpoint_id: 'TestWorker', feature_category: 'projects', queue: nil }) + end end end end RSpec.shared_examples 'metric with labels' do |metric_method| + before do + test_worker_class = Class.new do + def self.queue + 'test_worker' + end + end + stub_const('TestWorker', test_worker_class) + end + it 'measures with correct labels and value' do value = 1 - expect(prometheus_metric).to receive(metric_method).with({ endpoint_id: 'TestWorker', feature_category: 'projects' }, value) + expect(prometheus_metric).to receive(metric_method).with({ + endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' + }, value) Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do transaction.send(metric_method, :test_metric, value) diff --git a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb index 0b1424133b1..32d1288c59c 100644 --- a/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_non_sql_metrics_spec.rb @@ -34,4 +34,22 @@ RSpec.describe Gitlab::UsageDataNonSqlMetrics do expect(described_class.histogram(JiraImportState.finished, :imported_issues_count, buckets: [], bucket_size: 0)).to eq(default_count) end end + + describe 'min/max methods' do + using RSpec::Parameterized::TableSyntax + + where(:model, :result) do + User | nil + Issue | nil + Deployment | nil + Project | nil + end + + with_them do + it 'returns nil' do + expect(described_class.minimum_id(model)).to eq(result) + expect(described_class.maximum_id(model)).to eq(result) + end + end + end end diff --git a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb new file mode 100644 index 00000000000..1d35da528e4 --- /dev/null +++ b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20210302150310_schedule_migrate_pages_to_zip_storage.rb') + +RSpec.describe ScheduleMigratePagesToZipStorage, :sidekiq_might_not_need_inline, schema: 20201231133921 do + let(:migration_class) { described_class::MIGRATION } + let(:migration_name) { migration_class.to_s.demodulize } + + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:metadata_table) { table(:project_pages_metadata) } + let(:deployments_table) { table(:pages_deployments) } + + let(:namespace) { namespaces_table.create!(path: "group", name: "group") } + + def create_project_metadata(path, deployed, with_deployment) + project = projects_table.create!(path: path, namespace_id: namespace.id) + + deployment_id = nil + + if with_deployment + deployment_id = deployments_table.create!(project_id: project.id, file_store: 1, file: '1', file_count: 1, file_sha256: '123', size: 1).id + end + + metadata_table.create!(project_id: project.id, deployed: deployed, pages_deployment_id: deployment_id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + freeze_time do + create_project_metadata("not-deployed-project", false, false) + + first_id = create_project_metadata("project1", true, false).id + last_id = create_project_metadata("project2", true, false).id + + create_project_metadata("project-with-deployment", true, true) + + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(5.minutes, first_id, last_id) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + end + end +end diff --git a/spec/models/bulk_imports/stage_spec.rb b/spec/models/bulk_imports/stage_spec.rb index f14e9425a06..7765fd4c5c4 100644 --- a/spec/models/bulk_imports/stage_spec.rb +++ b/spec/models/bulk_imports/stage_spec.rb @@ -12,10 +12,10 @@ RSpec.describe BulkImports::Stage do [1, BulkImports::Groups::Pipelines::LabelsPipeline], [1, BulkImports::Groups::Pipelines::MilestonesPipeline], [1, BulkImports::Groups::Pipelines::BadgesPipeline], - [1, 'EE::BulkImports::Groups::Pipelines::IterationsPipeline'.constantize], - [2, 'EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize], - [3, 'EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize], - [3, 'EE::BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize], + [1, 'BulkImports::Groups::Pipelines::IterationsPipeline'.constantize], + [2, 'BulkImports::Groups::Pipelines::EpicsPipeline'.constantize], + [3, 'BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize], + [3, 'BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize], [4, BulkImports::Groups::Pipelines::EntityFinisher] ] else diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 958fe8d9455..b7f5811e945 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3902,6 +3902,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do pipeline.drop end end + + context 'with failure_reason' do + let(:pipeline) { create(:ci_pipeline, :running) } + let(:failure_reason) { 'config_error' } + let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') } + + it 'increments the counter with the failure_reason' do + expect { pipeline.drop!(failure_reason) }.to change { counter.get(reason: failure_reason) }.by(1) + end + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 1e0a875de73..e64dee2d26f 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -629,30 +629,45 @@ RSpec.describe CommitStatus do end end - describe 'set failure_reason when drop' do + describe '#drop' do let(:commit_status) { create(:commit_status, :created) } + let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_job_failure_reasons, 'desc') } + let(:failure_reason) { reason.to_s } subject do commit_status.drop!(reason) commit_status end + shared_examples 'incrementing failure reason counter' do + it 'increments the counter with the failure_reason' do + expect { subject }.to change { counter.get(reason: failure_reason) }.by(1) + end + end + context 'when failure_reason is nil' do let(:reason) { } + let(:failure_reason) { 'unknown_failure' } it { is_expected.to be_unknown_failure } + + it_behaves_like 'incrementing failure reason counter' end context 'when failure_reason is script_failure' do let(:reason) { :script_failure } it { is_expected.to be_script_failure } + + it_behaves_like 'incrementing failure reason counter' end context 'when failure_reason is unmet_prerequisites' do let(:reason) { :unmet_prerequisites } it { is_expected.to be_unmet_prerequisites } + + it_behaves_like 'incrementing failure reason counter' end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 1df70f38707..14db9b530db 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -65,6 +65,23 @@ RSpec.describe Issuable do it { expect(issuable_class).to respond_to(:opened) } it { expect(issuable_class).to respond_to(:closed) } it { expect(issuable_class).to respond_to(:assigned) } + + describe '.includes_for_bulk_update' do + before do + stub_const('Example', Class.new(ActiveRecord::Base)) + + Example.class_eval do + include Issuable # adds :labels and :metrics, among others + + belongs_to :author + has_many :assignees + end + end + + it 'includes available associations' do + expect(Example.includes_for_bulk_update.includes_values).to eq([:author, :assignees, :labels, :metrics]) + end + end end describe 'author_name' do diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb index 5fb3b39f734..961eac4710d 100644 --- a/spec/models/concerns/milestoneable_spec.rb +++ b/spec/models/concerns/milestoneable_spec.rb @@ -50,13 +50,13 @@ RSpec.describe Milestoneable do it 'returns true with a milestone from the issue project' do milestone = create(:milestone, project: project) - expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy + expect(build_milestoneable(milestone.id).milestone_available?).to be(true) end it 'returns true with a milestone from the issue project group' do milestone = create(:milestone, group: group) - expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy + expect(build_milestoneable(milestone.id).milestone_available?).to be(true) end it 'returns true with a milestone from the the parent of the issue project group' do @@ -64,19 +64,23 @@ RSpec.describe Milestoneable do group.update!(parent: parent) milestone = create(:milestone, group: parent) - expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy + expect(build_milestoneable(milestone.id).milestone_available?).to be(true) + end + + it 'returns true with a blank milestone' do + expect(build_milestoneable('').milestone_available?).to be(true) end it 'returns false with a milestone from another project' do milestone = create(:milestone) - expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey + expect(build_milestoneable(milestone.id).milestone_available?).to be(false) end it 'returns false with a milestone from another group' do milestone = create(:milestone, group: create(:group)) - expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey + expect(build_milestoneable(milestone.id).milestone_available?).to be(false) end end end diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb index 029eb8e513a..a27d836e2c2 100644 --- a/spec/models/pages_deployment_spec.rb +++ b/spec/models/pages_deployment_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe PagesDeployment do + let_it_be(:project) { create(:project) } + describe 'associations' do it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:ci_build).optional } @@ -28,7 +30,6 @@ RSpec.describe PagesDeployment do describe '.migrated_from_legacy_storage' do it 'only returns migrated deployments' do - project = create(:project) migrated_deployment = create_migrated_deployment(project) # create one other deployment create(:pages_deployment, project: project) @@ -37,6 +38,27 @@ RSpec.describe PagesDeployment do end end + context 'with deployments stored locally and remotely' do + before do + stub_pages_object_storage(::Pages::DeploymentUploader) + end + + let!(:remote_deployment) { create(:pages_deployment, project: project, file_store: ::ObjectStorage::Store::REMOTE) } + let!(:local_deployment) { create(:pages_deployment, project: project, file_store: ::ObjectStorage::Store::LOCAL) } + + describe '.with_files_stored_locally' do + it 'only returns deployments with files stored locally' do + expect(described_class.with_files_stored_locally).to contain_exactly(local_deployment) + end + end + + describe '.with_files_stored_remotely' do + it 'only returns deployments with files stored remotely' do + expect(described_class.with_files_stored_remotely).to contain_exactly(remote_deployment) + end + end + end + describe '#migrated?' do it 'returns false for normal deployment' do deployment = create(:pages_deployment) @@ -45,7 +67,6 @@ RSpec.describe PagesDeployment do end it 'returns true for migrated deployment' do - project = create(:project) deployment = create_migrated_deployment(project) expect(deployment.migrated?).to eq(true) @@ -67,7 +88,6 @@ RSpec.describe PagesDeployment do end describe 'default for file_store' do - let(:project) { create(:project) } let(:deployment) do filepath = Rails.root.join("spec/fixtures/pages.zip") diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb new file mode 100644 index 00000000000..db124c2252e --- /dev/null +++ b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases do + let_it_be(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + subject { described_class.new(context) } + + describe '#render?' do + context 'when project repository is empty' do + it 'returns false' do + allow(project).to receive(:empty_repo?).and_return(true) + + expect(subject.render?).to eq false + end + end + + context 'when project repository is not empty' do + context 'when user can read releases' do + it 'returns true' do + expect(subject.render?).to eq true + end + end + + context 'when user cannot read releases' do + let(:user) { nil } + + it 'returns false' do + expect(subject.render?).to eq false + end + end + end + end +end diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb new file mode 100644 index 00000000000..105a28ce953 --- /dev/null +++ b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::ProjectOverview::Menu do + let(:project) { build(:project) } + let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } + + subject { described_class.new(context) } + + it 'has the required items' do + items = subject.instance_variable_get(:@items) + + expect(items[0]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Details) + expect(items[1]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Activity) + expect(items[2]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases) + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index 6436fe1e9ef..0a5bcc7a965 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -235,4 +235,51 @@ RSpec.describe 'getting pipeline information nested in a project' do end end end + + context 'when requesting a specific test suite' do + let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } + let(:suite_name) { 'test' } + let_it_be(:build_ids) { pipeline.latest_builds.pluck(:id) } + + let(:variables) do + { + path: project.full_path, + pipelineIID: pipeline.iid.to_s + } + end + + let(:query) do + <<~GQL + query($path: ID!, $pipelineIID: ID!, $buildIds: [ID!]!) { + project(fullPath: $path) { + pipeline(iid: $pipelineIID) { + testSuite(buildIds: $buildIds) { + name + } + } + } + } + GQL + end + + it 'can request a test suite by an array of build_ids' do + vars = variables.merge(buildIds: build_ids) + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_data_at(:project, :pipeline, :testSuite, :name)).to eq(suite_name) + end + + context 'when pipeline has no builds that matches the given build_ids' do + let_it_be(:build_ids) { [non_existing_record_id] } + + it 'returns nil' do + vars = variables.merge(buildIds: build_ids) + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_data_at(*path, :test_suite)).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb new file mode 100644 index 00000000000..12f6fbd793e --- /dev/null +++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting blobs in a project repository' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:paths) { ["CONTRIBUTING.md", "README.md"] } + let(:ref) { project.default_branch } + let(:fields) do + <<~QUERY + blobs(paths:#{paths.inspect}, ref:#{ref.inspect}) { + nodes { + #{all_graphql_fields_for('repository_blob'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('repository', {}, fields) + ) + end + + subject(:blobs) { graphql_data_at(:project, :repository, :blobs, :nodes) } + + it 'returns the blob' do + post_graphql(query, current_user: current_user) + + expect(blobs).to match_array(paths.map { |path| a_hash_including('path' => path) }) + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 35f4b97df0a..98c85234fe7 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -71,19 +71,21 @@ RSpec.describe Ci::CreatePipelineService do end it 'increments the prometheus counter' do - expect(Gitlab::Metrics).to receive(:counter) - .with(:pipelines_created_total, "Counter of pipelines created") - .and_call_original - allow(Gitlab::Metrics).to receive(:counter).and_call_original # allow other counters + counter = spy('pipeline created counter') + + allow(Gitlab::Ci::Pipeline::Metrics) + .to receive(:pipelines_created_counter).and_return(counter) pipeline + + expect(counter).to have_received(:increment) end it 'records pipeline size in a prometheus histogram' do histogram = spy('pipeline size histogram') allow(Gitlab::Ci::Pipeline::Metrics) - .to receive(:new).and_return(histogram) + .to receive(:pipeline_size_histogram).and_return(histogram) execute_service @@ -580,6 +582,13 @@ RSpec.describe Ci::CreatePipelineService do it_behaves_like 'a failed pipeline' + it 'increments the error metric' do + stub_ci_pipeline_yaml_file(ci_yaml) + + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { execute_service }.to change { counter.get(reason: 'config_error') }.by(1) + end + context 'when receive git commit' do before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index e02536fd07f..254bd19c808 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -10,6 +10,14 @@ RSpec.describe Ci::ProcessPipelineService do create(:ci_empty_pipeline, ref: 'master', project: project) end + let(:pipeline_processing_events_counter) { double(increment: true) } + let(:legacy_update_jobs_counter) { double(increment: true) } + + let(:metrics) do + double(pipeline_processing_events_counter: pipeline_processing_events_counter, + legacy_update_jobs_counter: legacy_update_jobs_counter) + end + subject { described_class.new(pipeline) } before do @@ -17,22 +25,13 @@ RSpec.describe Ci::ProcessPipelineService do stub_not_protect_default_branch project.add_developer(user) + + allow(subject).to receive(:metrics).and_return(metrics) end describe 'processing events counter' do - let(:metrics) { double('pipeline metrics') } - let(:counter) { double('events counter') } - - before do - allow(subject) - .to receive(:metrics).and_return(metrics) - allow(metrics) - .to receive(:pipeline_processing_events_counter) - .and_return(counter) - end - it 'increments processing events counter' do - expect(counter).to receive(:increment) + expect(pipeline_processing_events_counter).to receive(:increment) subject.execute end @@ -64,33 +63,22 @@ RSpec.describe Ci::ProcessPipelineService do expect(all_builds.retried).to contain_exactly(build_retried) end - context 'counter ci_legacy_update_jobs_as_retried_total' do - let(:counter) { double(increment: true) } + it 'increments the counter' do + expect(legacy_update_jobs_counter).to receive(:increment) + subject.execute + end + + context 'when the previous build has already retried column true' do before do - allow(Gitlab::Metrics).to receive(:counter).and_call_original - allow(Gitlab::Metrics).to receive(:counter) - .with(:ci_legacy_update_jobs_as_retried_total, anything) - .and_return(counter) + build_retried.update_columns(retried: true) end - it 'increments the counter' do - expect(counter).to receive(:increment) + it 'does not increment the counter' do + expect(legacy_update_jobs_counter).not_to receive(:increment) subject.execute end - - context 'when the previous build has already retried column true' do - before do - build_retried.update_columns(retried: true) - end - - it 'does not increment the counter' do - expect(counter).not_to receive(:increment) - - subject.execute - end - end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 9346c92f98b..8c010855eb2 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -955,6 +955,40 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end context 'updating asssignee_ids' do + context ':use_specialized_service' do + context 'when true' do + it 'passes the update action to ::MergeRequests::UpdateAssigneesService' do + expect(::MergeRequests::UpdateAssigneesService) + .to receive(:new).and_call_original + + update_merge_request({ + assignee_ids: [user2.id], + use_specialized_service: true + }) + end + end + + context 'when false or nil' do + before do + expect(::MergeRequests::UpdateAssigneesService).not_to receive(:new) + end + + it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when false' do + update_merge_request({ + assignee_ids: [user2.id], + use_specialized_service: false + }) + end + + it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when nil' do + update_merge_request({ + assignee_ids: [user2.id], + use_specialized_service: nil + }) + end + end + end + it 'does not update assignee when assignee_id is invalid' do merge_request.update!(assignee_ids: [user.id]) diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb index 43c41b5c99d..3094f574184 100644 --- a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb +++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb @@ -85,26 +85,46 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do end describe 'experimentation' do - context 'when the experiment is enabled' do - it 'adds the group as an experiment subject in the experimental group' do - expect(Experiment).to receive(:add_group) - .with(:in_product_marketing_emails, variant: :experimental, group: group) - - execute_service - end - end - - context 'when the experiment is disabled' do - let(:experiment_enabled) { false } - - it 'adds the group as an experiment subject in the control group' do - expect(Experiment).to receive(:add_group) - .with(:in_product_marketing_emails, variant: :control, group: group) - - execute_service + context 'when on dotcom' do + before do + allow(::Gitlab).to receive(:com?).and_return(true) end - it { is_expected.not_to send_in_product_marketing_email } + context 'when the experiment is enabled' do + it 'adds the group as an experiment subject in the experimental group' do + expect(Experiment).to receive(:add_group) + .with(:in_product_marketing_emails, variant: :experimental, group: group) + + execute_service + end + end + + context 'when the experiment is disabled' do + let(:experiment_enabled) { false } + + it 'adds the group as an experiment subject in the control group' do + expect(Experiment).to receive(:add_group) + .with(:in_product_marketing_emails, variant: :control, group: group) + + execute_service + end + + it { is_expected.not_to send_in_product_marketing_email } + end + + context 'when not on dotcom' do + before do + allow(::Gitlab).to receive(:com?).and_return(false) + end + + it 'does not add the group as an experiment subject' do + expect(Experiment).not_to receive(:add_group) + + execute_service + end + + it { is_expected.to send_in_product_marketing_email(user.id, group.id, :create, 0) } + end end end diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb index 2a275bab4cc..d058324f3bb 100644 --- a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb +++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb @@ -5,104 +5,133 @@ require 'spec_helper' RSpec.describe Pages::MigrateFromLegacyStorageService do let(:batch_size) { 10 } let(:mark_projects_as_not_deployed) { false } - let(:service) { described_class.new(Rails.logger, migration_threads: 3, batch_size: batch_size, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) } + let(:service) { described_class.new(Rails.logger, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) } - it 'does not try to migrate pages if pages are not deployed' do - expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new) + shared_examples "migrates projects properly" do + it 'does not try to migrate pages if pages are not deployed' do + expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new) - expect(service.execute).to eq(migrated: 0, errored: 0) - end + is_expected.to eq(migrated: 0, errored: 0) + end - context 'when there is work for multiple threads' do - let(:batch_size) { 2 } # override to force usage of multiple threads + context 'when pages are marked as deployed' do + let(:project) { create(:project) } - it 'uses multiple threads' do - projects = create_list(:project, 20) - projects.each do |project| + before do project.mark_pages_as_deployed + end - FileUtils.mkdir_p File.join(project.pages_path, "public") - File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| - f.write("Hello!") + context 'when pages directory does not exist' do + context 'when mark_projects_as_not_deployed is set' do + let(:mark_projects_as_not_deployed) { true } + + it 'counts project as migrated' do + expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: true) do |service| + expect(service).to receive(:execute).and_call_original + end + + is_expected.to eq(migrated: 1, errored: 0) + end end - end - threads = Concurrent::Set.new - - expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args| - threads.add(Thread.current) - - # sleep to be 100% certain that once thread can't consume all the queue - # it works without it, but I want to avoid making this test flaky - sleep(0.01) - - m.call(*args) - end - - expect(service.execute).to eq(migrated: 20, errored: 0) - expect(threads.length).to eq(3) - end - end - - context 'when pages are marked as deployed' do - let(:project) { create(:project) } - - before do - project.mark_pages_as_deployed - end - - context 'when pages directory does not exist' do - context 'when mark_projects_as_not_deployed is set' do - let(:mark_projects_as_not_deployed) { true } - - it 'counts project as migrated' do - expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: true) do |service| + it 'counts project as errored' do + expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| expect(service).to receive(:execute).and_call_original end - expect(service.execute).to eq(migrated: 1, errored: 0) + is_expected.to eq(migrated: 0, errored: 1) end end - it 'counts project as errored' do - expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original - end - - expect(service.execute).to eq(migrated: 0, errored: 1) - end - end - - context 'when pages directory exists on disk' do - before do - FileUtils.mkdir_p File.join(project.pages_path, "public") - File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| - f.write("Hello!") - end - end - - it 'migrates pages projects without deployments' do - expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original - end - - expect do - expect(service.execute).to eq(migrated: 1, errored: 0) - end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil) - end - - context 'when deployed already exists for the project' do + context 'when pages directory exists on disk' do before do - deployment = create(:pages_deployment, project: project) - project.set_first_pages_deployment!(deployment) + FileUtils.mkdir_p File.join(project.pages_path, "public") + File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| + f.write("Hello!") + end end - it 'does not try to migrate project' do - expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new) + it 'migrates pages projects without deployments' do + expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| + expect(service).to receive(:execute).and_call_original + end - expect(service.execute).to eq(migrated: 0, errored: 0) + expect(project.pages_metadatum.reload.pages_deployment).to eq(nil) + expect(subject).to eq(migrated: 1, errored: 0) + expect(project.pages_metadatum.reload.pages_deployment).to be + end + + context 'when deployed already exists for the project' do + before do + deployment = create(:pages_deployment, project: project) + project.set_first_pages_deployment!(deployment) + end + + it 'does not try to migrate project' do + expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new) + + is_expected.to eq(migrated: 0, errored: 0) + end end end end end + + describe '#execute_with_threads' do + subject { service.execute_with_threads(threads: 3, batch_size: batch_size) } + + include_examples "migrates projects properly" + + context 'when there is work for multiple threads' do + let(:batch_size) { 2 } # override to force usage of multiple threads + + it 'uses multiple threads' do + projects = create_list(:project, 20) + projects.each do |project| + project.mark_pages_as_deployed + + FileUtils.mkdir_p File.join(project.pages_path, "public") + File.open(File.join(project.pages_path, "public/index.html"), "w") do |f| + f.write("Hello!") + end + end + + threads = Concurrent::Set.new + + expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args| + threads.add(Thread.current) + + # sleep to be 100% certain that once thread can't consume all the queue + # it works without it, but I want to avoid making this test flaky + sleep(0.01) + + m.call(*args) + end + + is_expected.to eq(migrated: 20, errored: 0) + expect(threads.length).to eq(3) + end + end + end + + describe "#execute_for_batch" do + subject { service.execute_for_batch(Project.ids) } + + include_examples "migrates projects properly" + + it 'only tries to migrate projects with passed ids' do + projects = create_list(:project, 5) + + projects.each(&:mark_pages_as_deployed) + projects_to_migrate = projects.first(3) + + projects_to_migrate.each do |project| + expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| + expect(service).to receive(:execute).and_call_original + end + end + + expect(service.execute_for_batch(projects_to_migrate.pluck(:id))).to eq(migrated: 0, errored: 3) + end + end end diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb index 1c5a803441d..664899c361b 100644 --- a/spec/tasks/gitlab/pages_rake_spec.rb +++ b/spec/tasks/gitlab/pages_rake_spec.rb @@ -12,11 +12,9 @@ RSpec.describe 'gitlab:pages' do it 'calls migration service' do expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, - migration_threads: 3, - batch_size: 10, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original + expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original end subject @@ -26,11 +24,9 @@ RSpec.describe 'gitlab:pages' do stub_env('PAGES_MIGRATION_THREADS', '5') expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, - migration_threads: 5, - batch_size: 10, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original + expect(service).to receive(:execute_with_threads).with(threads: 5, batch_size: 10).and_call_original end subject @@ -40,11 +36,9 @@ RSpec.describe 'gitlab:pages' do stub_env('PAGES_MIGRATION_BATCH_SIZE', '100') expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, - migration_threads: 3, - batch_size: 100, ignore_invalid_entries: false, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original + expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 100).and_call_original end subject @@ -54,11 +48,9 @@ RSpec.describe 'gitlab:pages' do stub_env('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'true') expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, - migration_threads: 3, - batch_size: 10, ignore_invalid_entries: true, mark_projects_as_not_deployed: false) do |service| - expect(service).to receive(:execute).and_call_original + expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original end subject @@ -68,11 +60,9 @@ RSpec.describe 'gitlab:pages' do stub_env('PAGES_MIGRATION_MARK_PROJECTS_AS_NOT_DEPLOYED', 'true') expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, - migration_threads: 3, - batch_size: 10, ignore_invalid_entries: false, mark_projects_as_not_deployed: true) do |service| - expect(service).to receive(:execute).and_call_original + expect(service).to receive(:execute_with_threads).with(threads: 3, batch_size: 10).and_call_original end subject @@ -96,4 +86,80 @@ RSpec.describe 'gitlab:pages' do expect(PagesDeployment.find_by_id(migrated_deployment.id)).to be_nil end end + + describe 'gitlab:pages:deployments:migrate_to_object_storage' do + subject { run_rake_task('gitlab:pages:deployments:migrate_to_object_storage') } + + before do + stub_pages_object_storage(::Pages::DeploymentUploader, enabled: object_storage_enabled) + end + + let!(:deployment) { create(:pages_deployment, file_store: store) } + let(:object_storage_enabled) { true } + + context 'when local storage is used' do + let(:store) { ObjectStorage::Store::LOCAL } + + context 'and remote storage is defined' do + it 'migrates file to remote storage' do + subject + + expect(deployment.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'and remote storage is not defined' do + let(:object_storage_enabled) { false } + + it 'fails to migrate to remote storage' do + subject + + expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end + + context 'when remote storage is used' do + let(:store) { ObjectStorage::Store::REMOTE } + + it 'file stays on remote storage' do + subject + + expect(deployment.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + end + + describe 'gitlab:pages:deployments:migrate_to_local' do + subject { run_rake_task('gitlab:pages:deployments:migrate_to_local') } + + before do + stub_pages_object_storage(::Pages::DeploymentUploader, enabled: object_storage_enabled) + end + + let!(:deployment) { create(:pages_deployment, file_store: store) } + let(:object_storage_enabled) { true } + + context 'when remote storage is used' do + let(:store) { ObjectStorage::Store::REMOTE } + + context 'and job has remote file store defined' do + it 'migrates file to local storage' do + subject + + expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end + + context 'when local storage is used' do + let(:store) { ObjectStorage::Store::LOCAL } + + it 'file stays on local storage' do + subject + + expect(deployment.reload.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end end diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 18467f95380..b6aff316b17 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -5,16 +5,51 @@ require 'spec_helper' RSpec.describe 'layouts/nav/sidebar/_project' do let_it_be_with_reload(:project) { create(:project, :repository) } + let(:user) { project.owner } + before do assign(:project, project) assign(:repository, project.repository) - allow(view).to receive(:current_ref).and_return('master') + allow(view).to receive(:current_ref).and_return('master') allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:current_user).and_return(user) end it_behaves_like 'has nav sidebar' + describe 'Project Overview' do + it 'has a link to the project path' do + render + + expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project rspec-project-link)) + end + + describe 'Details' do + it 'has a link to the projects path' do + render + + expect(rendered).to have_link('Details', href: project_path(project), class: 'shortcuts-project') + end + end + + describe 'Activity' do + it 'has a link to the project activity path' do + render + + expect(rendered).to have_link('Activity', href: activity_project_path(project), class: 'shortcuts-project-activity') + end + end + + describe 'Releases' do + it 'has a link to the project releases path' do + render + + expect(rendered).to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-project-releases') + end + end + end + describe 'issue boards' do it 'has board tab' do render @@ -99,19 +134,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end end - describe 'releases entry' do - it 'renders releases link' do - render - - expect(rendered).to have_link('Releases', href: project_releases_path(project)) - end - end - describe 'wiki entry tab' do let(:can_read_wiki) { true } before do - allow(view).to receive(:can?).with(nil, :read_wiki, project).and_return(can_read_wiki) + allow(view).to receive(:can?).with(user, :read_wiki, project).and_return(can_read_wiki) end describe 'when wiki is enabled' do @@ -299,7 +326,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do let(:read_cycle_analytics) { true } before do - allow(view).to receive(:can?).with(nil, :read_cycle_analytics, project).and_return(read_cycle_analytics) + allow(view).to receive(:can?).with(user, :read_cycle_analytics, project).and_return(read_cycle_analytics) end describe 'when value stream analytics is enabled' do