Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									5831f05b4c
								
							
						
					
					
						commit
						84b507d17b
					
				|  | @ -67,7 +67,6 @@ Layout/FirstHashElementIndentation: | |||
|     - 'ee/spec/controllers/projects_controller_spec.rb' | ||||
|     - 'ee/spec/elastic/migrate/migration_shared_examples.rb' | ||||
|     - 'ee/spec/factories/dependencies.rb' | ||||
|     - 'ee/spec/factories/licenses.rb' | ||||
|     - 'ee/spec/finders/epics_finder_spec.rb' | ||||
|     - 'ee/spec/finders/namespaces/free_user_cap/users_finder_spec.rb' | ||||
|     - 'ee/spec/frontend/fixtures/oncall_schedule.rb' | ||||
|  |  | |||
|  | @ -520,14 +520,6 @@ RSpec/FeatureCategory: | |||
|     - 'ee/spec/lib/banzai/reference_parser/epic_parser_spec.rb' | ||||
|     - 'ee/spec/lib/banzai/reference_parser/iteration_parser_spec.rb' | ||||
|     - 'ee/spec/lib/banzai/reference_parser/vulnerability_parser_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/groups/graphql/get_iterations_query_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/groups/pipelines/epics_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/groups/pipelines/iterations_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/bulk_imports/projects/pipelines/push_rule_pipeline_spec.rb' | ||||
|     - 'ee/spec/lib/compliance_management/merge_request_approval_settings/resolver_spec.rb' | ||||
|     - 'ee/spec/lib/container_registry/client_spec.rb' | ||||
|     - 'ee/spec/lib/ee/api/entities/analytics/code_review/merge_request_spec.rb' | ||||
|  | @ -2614,9 +2606,6 @@ RSpec/FeatureCategory: | |||
|     - 'spec/lib/bulk_imports/common/extractors/json_extractor_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/pipelines/badges_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/pipelines/members_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/rest/get_badges_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/file_downloads/filename_fetch_spec.rb' | ||||
|  | @ -2624,10 +2613,6 @@ RSpec/FeatureCategory: | |||
|     - 'spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/pipelines/group_attributes_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/pipelines/namespace_settings_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/groups/transformers/subgroup_to_entity_transformer_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/pipeline/context_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/pipeline/extracted_data_spec.rb' | ||||
|  | @ -2635,19 +2620,6 @@ RSpec/FeatureCategory: | |||
|     - 'spec/lib/bulk_imports/projects/graphql/get_project_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/graphql/get_repository_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/graphql/get_snippet_repository_query_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/project_attributes_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/project_feature_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/repository_bundle_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/service_desk_setting_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/retry_pipeline_error_spec.rb' | ||||
|     - 'spec/lib/bulk_imports/users_mapper_spec.rb' | ||||
|     - 'spec/lib/constraints/admin_constrainer_spec.rb' | ||||
|  |  | |||
|  | @ -122,6 +122,12 @@ export const MERGE_REQUEST_METRICS = { | |||
|   THROUGHPUT: MERGE_REQUEST_THROUGHPUT_TYPE, | ||||
| }; | ||||
| 
 | ||||
| export const CONTRIBUTOR_COUNT_TYPE = 'contributor_count'; | ||||
| 
 | ||||
| export const CONTRIBUTOR_METRICS = { | ||||
|   COUNT: CONTRIBUTOR_COUNT_TYPE, | ||||
| }; | ||||
| 
 | ||||
| export const METRIC_TOOLTIPS = { | ||||
|   [DORA_METRICS.DEPLOYMENT_FREQUENCY]: { | ||||
|     description: s__( | ||||
|  | @ -193,6 +199,15 @@ export const METRIC_TOOLTIPS = { | |||
|     projectLink: '-/analytics/merge_request_analytics', | ||||
|     docsLink: helpPagePath('user/analytics/merge_request_analytics'), | ||||
|   }, | ||||
|   [CONTRIBUTOR_METRICS.COUNT]: { | ||||
|     description: s__( | ||||
|       'ValueStreamAnalytics|Number of monthly unique users with contributions in the group.', | ||||
|     ), | ||||
|     groupLink: '-/contribution_analytics', | ||||
|     docsLink: helpPagePath('user/profile/contributions_calendar.html', { | ||||
|       anchor: 'user-contribution-events', | ||||
|     }), | ||||
|   }, | ||||
|   [VULNERABILITY_METRICS.CRITICAL]: { | ||||
|     description: s__('ValueStreamAnalytics|Critical vulnerabilities over time.'), | ||||
|     groupLink: '-/security/vulnerabilities?severity=CRITICAL', | ||||
|  |  | |||
|  | @ -219,7 +219,7 @@ class GfmAutoComplete { | |||
|         let tpl = '/${name} '; | ||||
|         let referencePrefix = null; | ||||
|         if (value.params.length > 0) { | ||||
|           const regexp = /\[[a-z]+:/; | ||||
|           const regexp = /^<\[[a-z]+:/; | ||||
|           const match = regexp.exec(value.params); | ||||
|           if (match) { | ||||
|             [referencePrefix] = match; | ||||
|  |  | |||
|  | @ -207,6 +207,16 @@ class BulkImports::Entity < ApplicationRecord | |||
|     @source_version ||= bulk_import.source_version_info | ||||
|   end | ||||
| 
 | ||||
|   def checksums | ||||
|     trackers.each_with_object({}) do |tracker, checksums| | ||||
|       next unless tracker.file_extraction_pipeline? | ||||
|       next if tracker.skipped? | ||||
|       next if tracker.checksums_empty? | ||||
| 
 | ||||
|       checksums.merge!(tracker.checksums) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def validate_parent_is_a_group | ||||
|  |  | |||
|  | @ -46,6 +46,10 @@ module BulkImports | |||
|       status['batches'].find { |item| item['batch_number'] == batch_number } | ||||
|     end | ||||
| 
 | ||||
|     def total_objects_count | ||||
|       status['total_objects_count'] | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     attr_reader :client, :entity, :relation, :pipeline_tracker | ||||
|  |  | |||
|  | @ -86,5 +86,42 @@ class BulkImports::Tracker < ApplicationRecord | |||
|     event :cleanup_stale do | ||||
|       transition [:created, :started] => :timeout | ||||
|     end | ||||
| 
 | ||||
|     after_transition any => [:finished, :failed] do |tracker| | ||||
|       BulkImports::ObjectCounter.persist!(tracker) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def checksums | ||||
|     return unless file_extraction_pipeline? | ||||
| 
 | ||||
|     # Return cached counters until they expire | ||||
|     { importing_relation => cached_checksums || persisted_checksums } | ||||
|   end | ||||
| 
 | ||||
|   def checksums_empty? | ||||
|     return true unless checksums | ||||
| 
 | ||||
|     sums = checksums[importing_relation] | ||||
| 
 | ||||
|     sums[:source] == 0 && sums[:fetched] == 0 && sums[:imported] == 0 | ||||
|   end | ||||
| 
 | ||||
|   def importing_relation | ||||
|     pipeline_class.relation.to_sym | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def cached_checksums | ||||
|     BulkImports::ObjectCounter.summary(self) | ||||
|   end | ||||
| 
 | ||||
|   def persisted_checksums | ||||
|     { | ||||
|       source: source_objects_count, | ||||
|       fetched: fetched_objects_count, | ||||
|       imported: imported_objects_count | ||||
|     } | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ module TimeTrackable | |||
|     @spent_at = options[:spent_at] | ||||
|     @summary = options[:summary] | ||||
|     @original_total_time_spent = nil | ||||
|     @category_id = category_id(options[:category]) | ||||
| 
 | ||||
|     return if @time_spent == 0 | ||||
| 
 | ||||
|  | @ -94,7 +95,8 @@ module TimeTrackable | |||
|       note_id: @time_spent_note_id, | ||||
|       user: @time_spent_user, | ||||
|       spent_at: @spent_at, | ||||
|       summary: @summary | ||||
|       summary: @summary, | ||||
|       timelog_category_id: @category_id | ||||
|     ) | ||||
|   end | ||||
|   # rubocop:enable Gitlab/ModuleWithInstanceVariables | ||||
|  | @ -119,4 +121,8 @@ module TimeTrackable | |||
| 
 | ||||
|     errors.add(:time_estimate, _('must have a valid format and be greater than or equal to zero.')) | ||||
|   end | ||||
| 
 | ||||
|   def category_id(category) | ||||
|     TimeTracking::TimelogCategory.find_by_name(project.root_namespace, category).first&.id | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -82,7 +82,12 @@ module BulkImports | |||
|     end | ||||
| 
 | ||||
|     def finish_export!(export) | ||||
|       export.update!(status_event: 'finish', batched: false, error: nil) | ||||
|       export.update!( | ||||
|         status_event: 'finish', | ||||
|         batched: false, | ||||
|         error: nil, | ||||
|         total_objects_count: export_service.exported_objects_count | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     def exported_filepath | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ Gitlab::HTTP_V2.configure do |config| | |||
|   end | ||||
| end | ||||
| 
 | ||||
| if Gitlab.config.gitlab['http_client'] | ||||
| if Gitlab.config.gitlab['http_client'].present? | ||||
|   pem = File.read(Gitlab.config.gitlab['http_client']['tls_client_cert_file']) | ||||
|   password = Gitlab.config.gitlab['http_client']['tls_client_cert_password'] | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,4 +7,12 @@ feature_categories: | |||
| description: Represents a Terraform state backend | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26619 | ||||
| milestone: '13.0' | ||||
| gitlab_schema: gitlab_main | ||||
| gitlab_schema: gitlab_main_cell | ||||
| allow_cross_joins: | ||||
| - gitlab_main_clusterwide | ||||
| allow_cross_transactions: | ||||
| - gitlab_main_clusterwide | ||||
| allow_cross_foreign_keys: | ||||
| - gitlab_main_clusterwide | ||||
| sharding_key: | ||||
|   project_id: projects | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddObjectCountFieldsToBulkImportTrackers < Gitlab::Database::Migration[2.2] | ||||
|   milestone '16.8' | ||||
| 
 | ||||
|   def change | ||||
|     add_column :bulk_import_trackers, :source_objects_count, :bigint, null: false, default: 0 | ||||
|     add_column :bulk_import_trackers, :fetched_objects_count, :bigint, null: false, default: 0 | ||||
|     add_column :bulk_import_trackers, :imported_objects_count, :bigint, null: false, default: 0 | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| 01a7d610bdf3c5d8e5f98f2c479a7cf83b7591e791b6f8e18f836b6bf6f52833 | ||||
|  | @ -13953,6 +13953,9 @@ CREATE TABLE bulk_import_trackers ( | |||
|     created_at timestamp with time zone, | ||||
|     updated_at timestamp with time zone, | ||||
|     batched boolean DEFAULT false, | ||||
|     source_objects_count bigint DEFAULT 0 NOT NULL, | ||||
|     fetched_objects_count bigint DEFAULT 0 NOT NULL, | ||||
|     imported_objects_count bigint DEFAULT 0 NOT NULL, | ||||
|     CONSTRAINT check_2d45cae629 CHECK ((char_length(relation) <= 255)), | ||||
|     CONSTRAINT check_40aeaa600b CHECK ((char_length(next_page) <= 255)), | ||||
|     CONSTRAINT check_603f91cb06 CHECK ((char_length(jid) <= 255)), | ||||
|  |  | |||
|  | @ -152,7 +152,19 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab | |||
|         "project_id": null, | ||||
|         "created_at": "2021-06-18T09:47:37.390Z", | ||||
|         "updated_at": "2021-06-18T09:47:51.867Z", | ||||
|         "failures": [] | ||||
|         "failures": [], | ||||
|         "stats": { | ||||
|             "labels": { | ||||
|                 "source": 10, | ||||
|                 "fetched": 10, | ||||
|                 "imported": 10 | ||||
|             }, | ||||
|             "milestones": { | ||||
|                 "source": 10, | ||||
|                 "fetched": 10, | ||||
|                 "imported": 10 | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "id": 2, | ||||
|  | @ -233,7 +245,19 @@ curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab | |||
|         "status": "finished", | ||||
|         "source_type": "gitlab", | ||||
|         "created_at": "2021-06-18T09:45:55.358Z", | ||||
|         "updated_at": "2021-06-18T09:46:27.003Z" | ||||
|         "updated_at": "2021-06-18T09:46:27.003Z", | ||||
|         "stats": { | ||||
|             "labels": { | ||||
|                 "source": 10, | ||||
|                 "fetched": 10, | ||||
|                 "imported": 10 | ||||
|             }, | ||||
|             "milestones": { | ||||
|                 "source": 10, | ||||
|                 "fetched": 10, | ||||
|                 "imported": 10 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| ] | ||||
| ``` | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| stage: core platform | ||||
| group: Tenant Scale | ||||
| description: 'Cells: Routing Service' | ||||
| status: accepted | ||||
| --- | ||||
| 
 | ||||
| # Cells: Routing Service | ||||
|  | @ -398,7 +399,7 @@ For the above example: | |||
| { | ||||
|     "metadata": { | ||||
|         "rule_id": "c9scvaiwj51a75kzoh917uwtnw8z4ebl", | ||||
|         "headers": {  | ||||
|         "headers": { | ||||
|             "all_request_headers": "value" | ||||
|         }, | ||||
|         "method": "GET", | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ The Value Streams Dashboard panels has a default configuration, but you can also | |||
| 
 | ||||
| ### DevSecOps metrics comparison panel | ||||
| 
 | ||||
| > Contributor count metric [added](https://gitlab.com/gitlab-org/gitlab/-/issues/433353) in GitLab 16.9. | ||||
| 
 | ||||
| The DevSecOps metrics comparison displays DORA4, vulnerability, and flow metrics for a group or project in the | ||||
| month-to-date, last month, the month before, and the past 180 days. | ||||
| 
 | ||||
|  | @ -48,6 +50,9 @@ that are the largest value contributors, overperforming, or underperforming. | |||
| You can also drill down the metrics for further analysis. | ||||
| When you hover over a metric, a tooltip displays an explanation of the metric and a link to the related documentation page. | ||||
| 
 | ||||
| NOTE: | ||||
| The contributor count metric is available only on GitLab.com at the group-level. To view this metric in the comparison panel, you must [set up ClickHouse](../../integration/clickhouse.md), and enable the [feature flags](../../administration/feature_flags.md) `clickhouse_data_collection` and `event_sync_worker_for_click_house`. | ||||
| 
 | ||||
| ### DORA Performers score panel | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/386843) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `dora_performers_score_panel`. Disabled by default. | ||||
|  | @ -242,6 +247,7 @@ If the comparison panel from the configuration file is enabled with `filter_labe | |||
| | Issues closed | Number of issues closed by month. | [Value Stream Analytics](https://gitlab.com/groups/gitlab-org/-/analytics/value_stream_analytics) | [Value Stream Analytics](../group/value_stream_analytics/index.md)  | `issues_completed` | | ||||
| | Number of deploys | Total number of deploys to production. | [Merge Request Analytics](https://gitlab.com/gitlab-org/gitlab/-/analytics/merge_request_analytics) | [Merge request analytics](merge_request_analytics.md) | `deploys` | | ||||
| | Merge request throughput | The number of merge requests merged by month. | [Groups Productivity analytics](productivity_analytics.md), [Projects Merge Request Analytics](https://gitlab.com/gitlab-org/gitlab/-/analytics/merge_request_analytics)  | [Groups Productivity analytics](productivity_analytics.md) [Projects Merge request analytics](merge_request_analytics.md) | `merge_request_throughput` | | ||||
| | Contributor count | Number of monthly unique users with contributions in the group.| [Contribution Analytics](https://gitlab.com/groups/gitlab-org/-/contribution_analytics) | [User contribution events](../profile/contributions_calendar.md#user-contribution-events) | `contributor_count` | | ||||
| | Critical vulnerabilities over time | Critical vulnerabilities over time in project or group | [Vulnerability report](https://gitlab.com/gitlab-org/gitlab/-/security/vulnerability_report) | [Vulnerability report](../application_security/vulnerability_report/index.md) | `vulnerability_critical` | | ||||
| | High vulnerabilities over time | High vulnerabilities over time in project or group | [Vulnerability report](https://gitlab.com/gitlab-org/gitlab/-/security/vulnerability_report) | [Vulnerability report](../application_security/vulnerability_report/index.md) | `vulnerability_high` | | ||||
| 
 | ||||
|  |  | |||
|  | @ -1047,6 +1047,8 @@ the note content. | |||
| Regardless of the tag names, the relative order of the reference tags determines the rendered | ||||
| numbering. | ||||
| 
 | ||||
| Regardless where you put the note, it's always shown at the bottom of the file. | ||||
| 
 | ||||
| <!-- | ||||
| The following codeblock uses HTML to skip the Vale ReferenceLinks test. | ||||
| Do not change it back to a markdown codeblock. | ||||
|  |  | |||
|  | @ -78,7 +78,7 @@ If you use [Slack slash commands](slack_slash_commands.md) or | |||
| - Replace `/gitlab` with the trigger name you've configured for these integrations. | ||||
| - Remove `<project>`. | ||||
| 
 | ||||
| The following slash commands are available in GitLab: | ||||
| The following slash commands are available for GitLab: | ||||
| 
 | ||||
| | Command | Description | | ||||
| | ------- | ----------- | | ||||
|  | @ -157,7 +157,7 @@ To receive notifications to a private Slack channel, you must add the GitLab for | |||
| 
 | ||||
| ### Notification events | ||||
| 
 | ||||
| The following GitLab events are available for Slack notifications: | ||||
| The following GitLab events can trigger notifications in Slack: | ||||
| 
 | ||||
| | Event                                                                | Description                                                   | | ||||
| |----------------------------------------------------------------------|---------------------------------------------------------------| | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ module API | |||
|         expose :failures, using: EntityFailure, documentation: { is_array: true } | ||||
|         expose :migrate_projects, documentation: { type: 'boolean', example: true } | ||||
|         expose :has_failures, documentation: { type: 'boolean', example: false } | ||||
|         expose :checksums, as: :stats, documentation: { type: 'object' } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -0,0 +1,69 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module BulkImports | ||||
|   class ObjectCounter | ||||
|     SOURCE_COUNTER = :source | ||||
|     FETCHED_COUNTER = :fetched | ||||
|     IMPORTED_COUNTER = :imported | ||||
|     COUNTER_TYPES = [SOURCE_COUNTER, FETCHED_COUNTER, IMPORTED_COUNTER].freeze | ||||
|     CACHE_KEY = 'bulk_imports/object_counter/%{tracker_id}' | ||||
| 
 | ||||
|     class << self | ||||
|       def increment(tracker, counter_type, value = 1) | ||||
|         return unless valid_input?(counter_type, value) | ||||
| 
 | ||||
|         Gitlab::Cache::Import::Caching.hash_increment(counter_key(tracker), counter_type, value) | ||||
|       end | ||||
| 
 | ||||
|       def set(tracker, counter_type, value = 1) | ||||
|         return unless valid_input?(counter_type, value) | ||||
| 
 | ||||
|         Gitlab::Cache::Import::Caching.hash_add(counter_key(tracker), counter_type, value) | ||||
|       end | ||||
| 
 | ||||
|       def summary(tracker) | ||||
|         object_counters = Gitlab::Cache::Import::Caching.values_from_hash(counter_key(tracker)) | ||||
| 
 | ||||
|         return unless object_counters.is_a?(Hash) | ||||
|         return if object_counters.empty? | ||||
| 
 | ||||
|         empty_response.merge(object_counters.symbolize_keys.transform_values(&:to_i)) | ||||
|       end | ||||
| 
 | ||||
|       # Commits counters from redis to the database | ||||
|       def persist!(tracker) | ||||
|         counters = summary(tracker) | ||||
| 
 | ||||
|         return unless counters | ||||
| 
 | ||||
|         tracker.update!( | ||||
|           source_objects_count: counters[SOURCE_COUNTER], | ||||
|           fetched_objects_count: counters[FETCHED_COUNTER], | ||||
|           imported_objects_count: counters[IMPORTED_COUNTER] | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def counter_key(tracker) | ||||
|         Kernel.format(CACHE_KEY, tracker_id: tracker.id) | ||||
|       end | ||||
| 
 | ||||
|       def valid_input?(counter_type, value) | ||||
|         return false unless value.is_a?(Integer) | ||||
|         return false if value <= 0 | ||||
|         return false unless COUNTER_TYPES.include?(counter_type) | ||||
| 
 | ||||
|         true | ||||
|       end | ||||
| 
 | ||||
|       def empty_response | ||||
|         { | ||||
|           SOURCE_COUNTER => 0, | ||||
|           FETCHED_COUNTER => 0, | ||||
|           IMPORTED_COUNTER => 0 | ||||
|         } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -12,6 +12,7 @@ module BulkImports | |||
| 
 | ||||
|         info(message: 'Pipeline started') | ||||
| 
 | ||||
|         set_source_objects_counter | ||||
|         extracted_data = extracted_data_from | ||||
| 
 | ||||
|         if extracted_data | ||||
|  | @ -21,6 +22,8 @@ module BulkImports | |||
|             raw_entry = entry.dup | ||||
|             next if already_processed?(raw_entry, index) | ||||
| 
 | ||||
|             increment_fetched_objects_counter | ||||
| 
 | ||||
|             transformers.each do |transformer| | ||||
|               entry = run_pipeline_step(:transformer, transformer.class.name) do | ||||
|                 transformer.transform(context, entry) | ||||
|  | @ -29,6 +32,8 @@ module BulkImports | |||
| 
 | ||||
|             run_pipeline_step(:loader, loader.class.name, entry) do | ||||
|               loader.load(context, entry) | ||||
| 
 | ||||
|               increment_imported_objects_counter | ||||
|             end | ||||
| 
 | ||||
|             save_processed_entry(raw_entry, index) | ||||
|  | @ -196,6 +201,21 @@ module BulkImports | |||
|         context.entity.touch | ||||
|         context.bulk_import.touch | ||||
|       end | ||||
| 
 | ||||
|       def set_source_objects_counter | ||||
|         # Export status is cached for 24h and read from Redis at this point | ||||
|         export_status = ExportStatus.new(tracker, tracker.importing_relation) | ||||
| 
 | ||||
|         ObjectCounter.set(tracker, ObjectCounter::SOURCE_COUNTER, export_status.total_objects_count) | ||||
|       end | ||||
| 
 | ||||
|       def increment_fetched_objects_counter | ||||
|         ObjectCounter.increment(tracker, ObjectCounter::FETCHED_COUNTER) | ||||
|       end | ||||
| 
 | ||||
|       def increment_imported_objects_counter | ||||
|         ObjectCounter.increment(tracker, ObjectCounter::IMPORTED_COUNTER) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module CloudConnector | ||||
|   module Config | ||||
|     extend self | ||||
| 
 | ||||
|     def base_url | ||||
|       Gitlab.config.cloud_connector.base_url | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -239,6 +239,25 @@ module Gitlab | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # Increments value of a field in a hash | ||||
|         # | ||||
|         # raw_key - The key of the hash to add to. | ||||
|         # field - The field to increment. | ||||
|         # value - The field value to add to the hash. | ||||
|         # timeout - The new timeout of the key. | ||||
|         def self.hash_increment(raw_key, field, value, timeout: TIMEOUT) | ||||
|           return if value.to_i <= 0 | ||||
| 
 | ||||
|           key = cache_key_for(raw_key) | ||||
| 
 | ||||
|           with_redis do |redis| | ||||
|             redis.multi do |m| | ||||
|               m.hincrby(key, field, value.to_i) | ||||
|               m.expire(key, timeout) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # Adds a value to a list. | ||||
|         # | ||||
|         # raw_key - The key of the list to add to. | ||||
|  |  | |||
|  | @ -181,7 +181,14 @@ module Gitlab | |||
|           spend_time_message(time_spent, time_spent_date, true) | ||||
|         end | ||||
| 
 | ||||
|         params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>' | ||||
|         params do | ||||
|           base_params = 'time(1h30m | -1h30m) <date(YYYY-MM-DD)>' | ||||
|           if Feature.enabled?(:timelog_categories, quick_action_target.project) | ||||
|             "#{base_params} <[timecategory:category-name]>" | ||||
|           else | ||||
|             base_params | ||||
|           end | ||||
|         end | ||||
|         types Issue, MergeRequest | ||||
|         condition do | ||||
|           quick_action_target.supports_time_tracking? && | ||||
|  | @ -190,12 +197,13 @@ module Gitlab | |||
|         parse_params do |raw_time_date| | ||||
|           Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute | ||||
|         end | ||||
|         command :spend, :spent, :spend_time do |time_spent, time_spent_date| | ||||
|         command :spend, :spent, :spend_time do |time_spent, time_spent_date, category| | ||||
|           if time_spent | ||||
|             @updates[:spend_time] = { | ||||
|               duration: time_spent, | ||||
|               user_id: current_user.id, | ||||
|               spent_at: time_spent_date | ||||
|               spent_at: time_spent_date, | ||||
|               category: category | ||||
|             } | ||||
|           end | ||||
|         end | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ module Gitlab | |||
|     # in other cases return nil | ||||
|     class SpendTimeAndDateSeparator | ||||
|       DATE_REGEX = %r{(\d{2,4}[/\-.]\d{1,2}[/\-.]\d{1,2})} | ||||
|       CATEGORY_REGEX = %r{\[timecategory:(.*)\]} | ||||
| 
 | ||||
|       def initialize(spend_command_arg) | ||||
|         @spend_arg = spend_command_arg | ||||
|  | @ -19,20 +20,23 @@ module Gitlab | |||
| 
 | ||||
|       def execute | ||||
|         return if @spend_arg.blank? | ||||
|         return [get_time, DateTime.current] unless date_present? | ||||
|         return unless valid_date? | ||||
|         return if date_present? && !valid_date? | ||||
| 
 | ||||
|         [get_time, get_date] | ||||
|         [time_spent, spent_at, category] | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def get_time | ||||
|       def time_spent | ||||
|         raw_time = @spend_arg.gsub(DATE_REGEX, '') | ||||
|         raw_time = raw_time.gsub(CATEGORY_REGEX, '') | ||||
| 
 | ||||
|         Gitlab::TimeTrackingFormatter.parse(raw_time) | ||||
|       end | ||||
| 
 | ||||
|       def get_date | ||||
|       def spent_at | ||||
|         return DateTime.current unless date_present? | ||||
| 
 | ||||
|         string_date = @spend_arg.match(DATE_REGEX)[0] | ||||
|         Date.parse(string_date) | ||||
|       end | ||||
|  | @ -55,6 +59,12 @@ module Gitlab | |||
|       def date_past_or_today?(date) | ||||
|         date&.past? || date&.today? | ||||
|       end | ||||
| 
 | ||||
|       def category | ||||
|         return unless @spend_arg.match?(CATEGORY_REGEX) | ||||
| 
 | ||||
|         @spend_arg.match(CATEGORY_REGEX)[1] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -15437,6 +15437,9 @@ msgstr "" | |||
| msgid "DORA4Metrics|Change failure rate (percentage)" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DORA4Metrics|Contributor count" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DORA4Metrics|Critical Vulnerabilities over time" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -33505,6 +33508,9 @@ msgstr "" | |||
| msgid "ObservabilityMetrics|is not like" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ObservabilityMetrics|multiple" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Observability|Enable" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -53962,6 +53968,9 @@ msgstr "" | |||
| msgid "ValueStreamAnalytics|Number of issues closed by month." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ValueStreamAnalytics|Number of monthly unique users with contributions in the group." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "ValueStreamAnalytics|Number of new issues created." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,6 +9,10 @@ FactoryBot.define do | |||
|     sequence(:pipeline_name) { |n| "pipeline_name_#{n}" } | ||||
|     sequence(:jid) { |n| "bulk_import_entity_#{n}" } | ||||
| 
 | ||||
|     source_objects_count { 1 } | ||||
|     fetched_objects_count { 1 } | ||||
|     imported_objects_count { 1 } | ||||
| 
 | ||||
|     trait :started do | ||||
|       status { 1 } | ||||
|     end | ||||
|  |  | |||
|  | @ -23,7 +23,8 @@ RSpec.describe API::Entities::BulkImports::Entity, feature_category: :importers | |||
|       :updated_at, | ||||
|       :failures, | ||||
|       :migrate_projects, | ||||
|       :has_failures | ||||
|       :has_failures, | ||||
|       :stats | ||||
|     ) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Common::Pipelines::BadgesPipeline do | ||||
| RSpec.describe BulkImports::Common::Pipelines::BadgesPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project) } | ||||
|  | @ -21,6 +21,8 @@ RSpec.describe BulkImports::Common::Pipelines::BadgesPipeline do | |||
|       allow_next_instance_of(BulkImports::Common::Extractors::RestExtractor) do |extractor| | ||||
|         allow(extractor).to receive(:extract).and_return(first_page, last_page) | ||||
|       end | ||||
| 
 | ||||
|       allow(subject).to receive(:set_source_objects_counter) | ||||
|     end | ||||
| 
 | ||||
|     it 'imports a group badge' do | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ RSpec.describe BulkImports::Common::Pipelines::BoardsPipeline, feature_category: | |||
|     allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| | ||||
|       allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: board_data)) | ||||
|     end | ||||
|     allow(subject).to receive(:set_source_objects_counter) | ||||
|     group.add_owner(user) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline, feature_category: | |||
|   let(:tmpdir) { Dir.mktmpdir } | ||||
| 
 | ||||
|   before do | ||||
|     allow(subject).to receive(:set_source_objects_counter) | ||||
|     FileUtils.copy_file(filepath, File.join(tmpdir, 'labels.ndjson.gz')) | ||||
|     group.add_owner(user) | ||||
|   end | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline, feature_categ | |||
|     File.write(lfs_json_file_path, { oid => [0, 1, 2, nil] }.to_json ) | ||||
| 
 | ||||
|     allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   after do | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Common::Pipelines::MembersPipeline do | ||||
| RSpec.describe BulkImports::Common::Pipelines::MembersPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:bulk_import) { create(:bulk_import, user: user) } | ||||
|   let_it_be(:member_user1) { create(:user, email: 'email1@email.com') } | ||||
|  | @ -25,6 +25,10 @@ RSpec.describe BulkImports::Common::Pipelines::MembersPipeline do | |||
| 
 | ||||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   def extracted_data(email:, has_next_page: false) | ||||
|     data = { | ||||
|       'created_at' => '2020-01-01T00:00:00Z', | ||||
|  |  | |||
|  | @ -48,6 +48,8 @@ RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline, feature_categ | |||
|     allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| | ||||
|       allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: exported_milestones)) | ||||
|     end | ||||
| 
 | ||||
|     allow(subject).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   subject { described_class.new(context) } | ||||
|  |  | |||
|  | @ -19,6 +19,8 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline, feature_category | |||
| 
 | ||||
|     FileUtils.mkdir_p(uploads_dir_path) | ||||
|     FileUtils.touch(upload_file_path) | ||||
| 
 | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   after do | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Groups::Pipelines::GroupAttributesPipeline do | ||||
| RSpec.describe BulkImports::Groups::Pipelines::GroupAttributesPipeline, feature_category: :importers do | ||||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   let_it_be(:user) { create(:user) } | ||||
|  | @ -35,6 +35,8 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupAttributesPipeline do | |||
|           BulkImports::Pipeline::ExtractedData.new(data: group_attributes) | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
|     end | ||||
| 
 | ||||
|     it 'imports allowed group attributes' do | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do | ||||
| RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline, feature_category: :importers do | ||||
|   describe '#run', :clean_gitlab_redis_cache do | ||||
|     let_it_be(:user) { create(:user) } | ||||
|     let_it_be(:parent) { create(:group) } | ||||
|  | @ -42,6 +42,8 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: group_data)) | ||||
|       end | ||||
| 
 | ||||
|       allow(subject).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       parent.add_owner(user) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Groups::Pipelines::NamespaceSettingsPipeline do | ||||
| RSpec.describe BulkImports::Groups::Pipelines::NamespaceSettingsPipeline, feature_category: :importers do | ||||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   let_it_be(:user) { create(:user) } | ||||
|  | @ -14,6 +14,8 @@ RSpec.describe BulkImports::Groups::Pipelines::NamespaceSettingsPipeline do | |||
| 
 | ||||
|   before do | ||||
|     group.add_owner(user) | ||||
| 
 | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   describe '#run' do | ||||
|  |  | |||
|  | @ -34,6 +34,8 @@ RSpec.describe BulkImports::Groups::Pipelines::ProjectEntitiesPipeline, feature_ | |||
|         allow(extractor).to receive(:extract).and_return(extracted_data) | ||||
|       end | ||||
| 
 | ||||
|       allow(subject).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       destination_group.add_owner(user) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do | ||||
| RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group, path: 'group') } | ||||
|   let_it_be(:parent) { create(:group, name: 'Imported Group', path: 'imported-group') } | ||||
|  | @ -25,6 +25,8 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(extracted_data) | ||||
|       end | ||||
| 
 | ||||
|       allow(subject).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       parent.add_owner(user) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,132 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::ObjectCounter, :clean_gitlab_redis_cache, feature_category: :importers do | ||||
|   let_it_be(:tracker) { build(:bulk_import_tracker, id: non_existing_record_id) } | ||||
|   let_it_be(:cache_key) { "bulk_imports/object_counter/#{tracker.id}" } | ||||
| 
 | ||||
|   describe '.increment' do | ||||
|     it 'increments counter by 1' do | ||||
|       expect(Gitlab::Cache::Import::Caching) | ||||
|         .to receive(:hash_increment) | ||||
|         .with(cache_key, described_class::SOURCE_COUNTER, 1) | ||||
| 
 | ||||
|       described_class.increment(tracker, described_class::SOURCE_COUNTER) | ||||
|     end | ||||
| 
 | ||||
|     it 'increments counter by given value' do | ||||
|       expect(Gitlab::Cache::Import::Caching) | ||||
|         .to receive(:hash_increment) | ||||
|         .with(cache_key, described_class::SOURCE_COUNTER, 10) | ||||
| 
 | ||||
|       described_class.increment(tracker, described_class::SOURCE_COUNTER, 10) | ||||
|     end | ||||
| 
 | ||||
|     context 'when value is not an integer' do | ||||
|       it 'does not increment counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_increment) | ||||
| 
 | ||||
|         described_class.increment(tracker, described_class::SOURCE_COUNTER, 'foo') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when value is less than 1' do | ||||
|       it 'does not increment counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_increment) | ||||
| 
 | ||||
|         described_class.increment(tracker, described_class::SOURCE_COUNTER, 0) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when counter type is invalid' do | ||||
|       it 'does not increment counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_increment) | ||||
| 
 | ||||
|         described_class.increment(tracker, 'foo') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.set' do | ||||
|     it 'sets counter to given value' do | ||||
|       expect(Gitlab::Cache::Import::Caching).to receive(:hash_add).with(cache_key, described_class::SOURCE_COUNTER, 10) | ||||
| 
 | ||||
|       described_class.set(tracker, described_class::SOURCE_COUNTER, 10) | ||||
|     end | ||||
| 
 | ||||
|     context 'when value is not an integer' do | ||||
|       it 'does not set counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_add) | ||||
| 
 | ||||
|         described_class.set(tracker, described_class::SOURCE_COUNTER, 'foo') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when value is less than 1' do | ||||
|       it 'does not set counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_add) | ||||
| 
 | ||||
|         described_class.set(tracker, described_class::SOURCE_COUNTER, 0) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when counter type is invalid' do | ||||
|       it 'does not set counter' do | ||||
|         expect(Gitlab::Cache::Import::Caching).not_to receive(:hash_add) | ||||
| 
 | ||||
|         described_class.set(tracker, 'foo') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.summary' do | ||||
|     it 'returns symbolized hash' do | ||||
|       expect(Gitlab::Cache::Import::Caching) | ||||
|         .to receive(:values_from_hash) | ||||
|         .with(cache_key).and_return({ 'source' => 10 }) | ||||
| 
 | ||||
|       expect(described_class.summary(tracker)).to eq(source: 10, fetched: 0, imported: 0) | ||||
|     end | ||||
| 
 | ||||
|     context 'when hash is empty' do | ||||
|       it 'returns nil' do | ||||
|         expect(Gitlab::Cache::Import::Caching).to receive(:values_from_hash).with(cache_key).and_return({}) | ||||
| 
 | ||||
|         expect(described_class.summary(tracker)).to be_nil | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when return value is not a hash' do | ||||
|       it 'returns nil' do | ||||
|         expect(Gitlab::Cache::Import::Caching).to receive(:values_from_hash).with(cache_key).and_return('foo') | ||||
| 
 | ||||
|         expect(described_class.summary(tracker)).to be_nil | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.persist!' do | ||||
|     it 'updates tracker with summary' do | ||||
|       tracker = create( | ||||
|         :bulk_import_tracker, | ||||
|         source_objects_count: 0, | ||||
|         fetched_objects_count: 0, | ||||
|         imported_objects_count: 0 | ||||
|       ) | ||||
| 
 | ||||
|       expect(Gitlab::Cache::Import::Caching) | ||||
|         .to receive(:values_from_hash) | ||||
|         .with("bulk_imports/object_counter/#{tracker.id}") | ||||
|         .and_return('source' => 10, 'fetched' => 20, 'imported' => 30) | ||||
| 
 | ||||
|       described_class.persist!(tracker) | ||||
| 
 | ||||
|       tracker.reload | ||||
| 
 | ||||
|       expect(tracker.source_objects_count).to eq(10) | ||||
|       expect(tracker.fetched_objects_count).to eq(20) | ||||
|       expect(tracker.imported_objects_count).to eq(30) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -41,6 +41,12 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do | |||
|     end | ||||
| 
 | ||||
|     stub_const('BulkImports::MyPipeline', pipeline) | ||||
| 
 | ||||
|     allow_next_instance_of(BulkImports::ExportStatus) do |export_status| | ||||
|       allow(export_status).to receive(:total_objects_count).and_return(1) | ||||
|     end | ||||
| 
 | ||||
|     allow(tracker).to receive_message_chain(:pipeline_class, :relation).and_return('relation') | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:bulk_import) { create(:bulk_import) } | ||||
|  | @ -433,6 +439,35 @@ RSpec.describe BulkImports::Pipeline::Runner, feature_category: :importers do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     describe 'object counting' do | ||||
|       it 'increments object counters' do | ||||
|         allow_next_instance_of(BulkImports::Extractor) do |extractor| | ||||
|           allow(extractor).to receive(:extract).with(context).and_return(extracted_data) | ||||
|         end | ||||
| 
 | ||||
|         allow_next_instance_of(BulkImports::Transformer) do |transformer| | ||||
|           allow(transformer) | ||||
|             .to receive(:transform) | ||||
|             .with(context, extracted_data.data.first) | ||||
|             .and_return(extracted_data.data.first) | ||||
|         end | ||||
| 
 | ||||
|         allow_next_instance_of(BulkImports::Loader) do |loader| | ||||
|           expect(loader).to receive(:load).with(context, extracted_data.data.first) | ||||
|         end | ||||
| 
 | ||||
|         expect(BulkImports::ObjectCounter).to receive(:set).with(tracker, :source, 1) | ||||
|         expect(BulkImports::ObjectCounter).to receive(:increment).with(tracker, :fetched) | ||||
|         expect(BulkImports::ObjectCounter).to receive(:increment).with(tracker, :imported) | ||||
| 
 | ||||
|         subject.run | ||||
| 
 | ||||
|         expect(tracker.source_objects_count).to eq(1) | ||||
|         expect(tracker.fetched_objects_count).to eq(1) | ||||
|         expect(tracker.imported_objects_count).to eq(1) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def log_params(context, extra = {}) | ||||
|       { | ||||
|         bulk_import_id: context.bulk_import_id, | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::AutoDevopsPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::AutoDevopsPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project, group: group) } | ||||
|  | @ -41,6 +41,8 @@ RSpec.describe BulkImports::Projects::Pipelines::AutoDevopsPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [auto_devops])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
| 
 | ||||
|       expect(project.auto_devops.enabled).to be_truthy | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project, group: group) } | ||||
|  | @ -43,6 +43,10 @@ RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do | |||
| 
 | ||||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   describe '#run', :clean_gitlab_redis_cache do | ||||
|     before do | ||||
|       group.add_owner(user) | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ RSpec.describe BulkImports::Projects::Pipelines::CommitNotesPipeline, feature_ca | |||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   describe '#run' do | ||||
|     before do | ||||
|     it 'imports ci pipeline notes into destination project' do | ||||
|       group.add_owner(user) | ||||
| 
 | ||||
|       allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| | ||||
|  | @ -60,9 +60,9 @@ RSpec.describe BulkImports::Projects::Pipelines::CommitNotesPipeline, feature_ca | |||
|           BulkImports::Pipeline::ExtractedData.new(data: [ci_pipeline_note]) | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'imports ci pipeline notes into destination project' do | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       expect { pipeline.run }.to change { project.notes.for_commit_id("sha-notes").count }.from(0).to(1) | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -30,6 +30,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ContainerExpirationPolicyPipeli | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[policy, 0]])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
| 
 | ||||
|       policy.each_pair do |key, value| | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline, feature_c | |||
| 
 | ||||
|     allow(portable).to receive(:lfs_enabled?).and_return(true) | ||||
|     allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   after do | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline, f | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[external_pull_request, 0]])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project, group: group) } | ||||
|  | @ -45,6 +45,8 @@ RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [issue_with_index])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -101,6 +101,7 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline, feature_ | |||
|       allow(project.repository).to receive(:create_branch) | ||||
| 
 | ||||
|       allow(::Projects::ImportExport::AfterImportMergeRequestsWorker).to receive(:perform_async) | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
|     end | ||||
|  |  | |||
|  | @ -43,6 +43,8 @@ RSpec.describe BulkImports::Projects::Pipelines::PipelineSchedulesPipeline, :cle | |||
|       allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [schedule])) | ||||
|     end | ||||
| 
 | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|     pipeline.run | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline, :with_license do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline, :with_license, feature_category: :importers do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:bulk_import) { create(:bulk_import) } | ||||
|   let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) } | ||||
|  | @ -62,6 +62,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectAttributesPipeline, :wit | |||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProjectFeaturePipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProjectFeaturePipeline, feature_category: :importers do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|  | @ -35,6 +35,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectFeaturePipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[project_feature, 0]])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
| 
 | ||||
|       project_feature.each_pair do |key, value| | ||||
|  |  | |||
|  | @ -36,6 +36,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectPipeline, feature_catego | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: project_data)) | ||||
|       end | ||||
| 
 | ||||
|       allow(project_pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       group.add_owner(user) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProtectedBranchesPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ProtectedBranchesPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:bulk_import) { create(:bulk_import, user: user) } | ||||
|  | @ -39,6 +39,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ProtectedBranchesPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [protected_branch, 0])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
| 
 | ||||
|       imported_protected_branch = project.protected_branches.last | ||||
|  |  | |||
|  | @ -37,6 +37,10 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline, feature_cat | |||
| 
 | ||||
|   subject(:pipeline) { described_class.new(context) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(subject).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   describe '#run' do | ||||
|     it "enqueues TransformReferencesWorker for the project's issues, mrs and their notes" do | ||||
|       expect(BulkImports::TransformReferencesWorker).to receive(:perform_in) | ||||
|  |  | |||
|  | @ -46,6 +46,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline, feature_categ | |||
|       allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| | ||||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [with_index])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
|     end | ||||
| 
 | ||||
|     it 'imports release into destination project' do | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ RSpec.describe BulkImports::Projects::Pipelines::RepositoryBundlePipeline, featu | |||
|     source.repository.bundle_to_disk(bundle_path) | ||||
| 
 | ||||
|     allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   after do | ||||
|  |  | |||
|  | @ -32,6 +32,8 @@ RSpec.describe BulkImports::Projects::Pipelines::RepositoryPipeline, feature_cat | |||
|     allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| | ||||
|       allow(extractor).to receive(:extract).and_return(extracted_data) | ||||
|     end | ||||
| 
 | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   describe '#run' do | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline, feature_category: :importers do | ||||
|   let_it_be(:project) { create(:project) } | ||||
|   let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project) } | ||||
|   let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } | ||||
|  | @ -17,6 +17,8 @@ RSpec.describe BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[setting, 0]])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
| 
 | ||||
|       setting.each_pair do |key, value| | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe BulkImports::Projects::Pipelines::SnippetsPipeline do | ||||
| RSpec.describe BulkImports::Projects::Pipelines::SnippetsPipeline, feature_category: :importers do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let_it_be(:group) { create(:group) } | ||||
|   let_it_be(:project) { create(:project, group: group) } | ||||
|  | @ -49,6 +49,8 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsPipeline do | |||
|         allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [snippet_with_index])) | ||||
|       end | ||||
| 
 | ||||
|       allow(pipeline).to receive(:set_source_objects_counter) | ||||
| 
 | ||||
|       pipeline.run | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -45,6 +45,10 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline, fea | |||
| 
 | ||||
|   let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(pipeline).to receive(:set_source_objects_counter) | ||||
|   end | ||||
| 
 | ||||
|   describe 'extractor' do | ||||
|     it 'is a GraphqlExtractor with Graphql::GetSnippetRepositoryQuery' do | ||||
|       expect(described_class.get_extractor).to eq( | ||||
|  |  | |||
|  | @ -171,6 +171,40 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_cache, :clean | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.hash_increment' do | ||||
|     it 'increments a value in a hash' do | ||||
|       described_class.hash_increment('foo', 'field', 1) | ||||
|       described_class.hash_increment('foo', 'field', 5) | ||||
| 
 | ||||
|       key = described_class.cache_key_for('foo') | ||||
|       values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } | ||||
| 
 | ||||
|       expect(values).to eq({ 'field' => '6' }) | ||||
|     end | ||||
| 
 | ||||
|     context 'when the value is not an integer' do | ||||
|       it 'returns' do | ||||
|         described_class.hash_increment('another-foo', 'another-field', 'not-an-integer') | ||||
| 
 | ||||
|         key = described_class.cache_key_for('foo') | ||||
|         values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } | ||||
| 
 | ||||
|         expect(values).to eq({}) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when the value is less than 0' do | ||||
|       it 'returns' do | ||||
|         described_class.hash_increment('another-foo', 'another-field', -5) | ||||
| 
 | ||||
|         key = described_class.cache_key_for('foo') | ||||
|         values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } | ||||
| 
 | ||||
|         expect(values).to eq({}) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.write_multiple' do | ||||
|     it 'sets multiple keys when key_prefix not set' do | ||||
|       mapping = { 'foo' => 10, 'bar' => 20 } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::QuickActions::SpendTimeAndDateSeparator do | ||||
| RSpec.describe Gitlab::QuickActions::SpendTimeAndDateSeparator, feature_category: :team_planning do | ||||
|   subject { described_class } | ||||
| 
 | ||||
|   shared_examples 'arg line with invalid parameters' do | ||||
|  | @ -51,23 +51,38 @@ RSpec.describe Gitlab::QuickActions::SpendTimeAndDateSeparator do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'only time present in arg line' do | ||||
|       it_behaves_like 'arg line with valid parameters' do | ||||
|         let(:valid_arg) { '2m 3m 5m 1h' } | ||||
|         let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) } | ||||
|         let(:date) { DateTime.current } | ||||
|         let(:expected_response) { [time, date] } | ||||
|     context 'time present in arg line' do | ||||
|       let(:time_part) { '2m 3m 5m 1h' } | ||||
|       let(:valid_arg) { time_part } | ||||
|       let(:time) { Gitlab::TimeTrackingFormatter.parse(time_part) } | ||||
|       let(:date) { DateTime.current } | ||||
|       let(:expected_response) { [time, date, nil] } | ||||
| 
 | ||||
|       it_behaves_like 'arg line with valid parameters' | ||||
| 
 | ||||
|       context 'timecategory present in arg line' do | ||||
|         let(:valid_arg) { "#{time_part} [timecategory:dev]" } | ||||
|         let(:expected_response) { [time, date, 'dev'] } | ||||
| 
 | ||||
|         it_behaves_like 'arg line with valid parameters' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'simple time with date in arg line' do | ||||
|       it_behaves_like 'arg line with valid parameters' do | ||||
|         let(:raw_time) { '10m' } | ||||
|         let(:raw_date) { '2016-02-02' } | ||||
|         let(:valid_arg) { "#{raw_time} #{raw_date}" } | ||||
|         let(:date) { Date.parse(raw_date) } | ||||
|         let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } | ||||
|         let(:expected_response) { [time, date] } | ||||
|       let(:raw_time) { '10m' } | ||||
|       let(:raw_date) { '2016-02-02' } | ||||
|       let(:valid_arg) { "#{raw_time} #{raw_date}" } | ||||
|       let(:date) { Date.parse(raw_date) } | ||||
|       let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } | ||||
|       let(:expected_response) { [time, date, nil] } | ||||
| 
 | ||||
|       it_behaves_like 'arg line with valid parameters' | ||||
| 
 | ||||
|       context 'timecategory present in arg line' do | ||||
|         let(:valid_arg) { "#{raw_time} #{raw_date} [timecategory:support]" } | ||||
|         let(:expected_response) { [time, date, 'support'] } | ||||
| 
 | ||||
|         it_behaves_like 'arg line with valid parameters' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -78,7 +93,7 @@ RSpec.describe Gitlab::QuickActions::SpendTimeAndDateSeparator do | |||
|         let(:valid_arg) { "#{raw_time} #{raw_date}" } | ||||
|         let(:date) { Date.parse(raw_date) } | ||||
|         let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } | ||||
|         let(:expected_response) { [time, date] } | ||||
|         let(:expected_response) { [time, date, nil] } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -487,4 +487,71 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d | |||
|       expect(subject.source_version).to eq(subject.bulk_import.source_version_info) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#checksums' do | ||||
|     let(:entity) { create(:bulk_import_entity) } | ||||
| 
 | ||||
|     it 'returns checksums for all imported relations' do | ||||
|       create(:bulk_import_tracker, | ||||
|         entity: entity, | ||||
|         relation: 'BulkImports::Common::Pipelines::MilestonesPipeline', | ||||
|         source_objects_count: 7, | ||||
|         fetched_objects_count: 6, | ||||
|         imported_objects_count: 5 | ||||
|       ) | ||||
| 
 | ||||
|       create(:bulk_import_tracker, | ||||
|         entity: entity, | ||||
|         relation: 'BulkImports::Common::Pipelines::LabelsPipeline', | ||||
|         source_objects_count: 10, | ||||
|         fetched_objects_count: 9, | ||||
|         imported_objects_count: 8 | ||||
|       ) | ||||
| 
 | ||||
|       expect(entity.checksums).to eq( | ||||
|         { | ||||
|           milestones: { | ||||
|             source: 7, | ||||
|             fetched: 6, | ||||
|             imported: 5 | ||||
|           }, | ||||
|           labels: { | ||||
|             source: 10, | ||||
|             fetched: 9, | ||||
|             imported: 8 | ||||
|           } | ||||
|         } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     context 'when tracker should not be included' do | ||||
|       let(:tracker) { create(:bulk_import_tracker, entity: entity, relation: 'BulkImports::Common::Pipelines::MilestonesPipeline') } | ||||
| 
 | ||||
|       context 'when tracker is for a file extraction pipeline' do | ||||
|         it 'does not include the tracker' do | ||||
|           expect(entity.checksums).to eq({}) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when tracker is skipped' do | ||||
|         it 'does not include the tracker' do | ||||
|           tracker.skip! | ||||
| 
 | ||||
|           expect(entity.checksums).to eq({}) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when tracker checksums are zeros' do | ||||
|         it 'does not include the tracker' do | ||||
|           tracker.update!( | ||||
|             source_objects_count: 0, | ||||
|             fetched_objects_count: 0, | ||||
|             imported_objects_count: 0 | ||||
|           ) | ||||
| 
 | ||||
|           expect(entity.checksums).to eq({}) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -389,4 +389,12 @@ RSpec.describe BulkImports::ExportStatus, :clean_gitlab_redis_cache, feature_cat | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#total_objects_count' do | ||||
|     let(:status) { BulkImports::Export::FINISHED } | ||||
| 
 | ||||
|     it 'returns total objects count' do | ||||
|       expect(subject.total_objects_count).to eq(1) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -110,4 +110,100 @@ RSpec.describe BulkImports::Tracker, type: :model, feature_category: :importers | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#checksums' do | ||||
|     let(:tracker) { create(:bulk_import_tracker) } | ||||
|     let(:checksums) { { source: 1, fetched: 1, imported: 1 } } | ||||
| 
 | ||||
|     before do | ||||
|       allow(tracker).to receive(:file_extraction_pipeline?).and_return(true) | ||||
|       allow(tracker).to receive_message_chain(:pipeline_class, :relation, :to_sym).and_return(:labels) | ||||
|     end | ||||
| 
 | ||||
|     context 'when checksums are cached' do | ||||
|       it 'returns the cached checksums' do | ||||
|         allow(BulkImports::ObjectCounter).to receive(:summary).and_return(checksums) | ||||
| 
 | ||||
|         expect(tracker.checksums).to eq({ labels: checksums }) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when checksums are persisted' do | ||||
|       it 'returns the persisted checksums' do | ||||
|         allow(BulkImports::ObjectCounter).to receive(:summary).and_return(nil) | ||||
| 
 | ||||
|         tracker.update!( | ||||
|           source_objects_count: checksums[:source], | ||||
|           fetched_objects_count: checksums[:fetched], | ||||
|           imported_objects_count: checksums[:imported] | ||||
|         ) | ||||
| 
 | ||||
|         expect(tracker.checksums).to eq({ labels: checksums }) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when pipeline is not a file extraction pipeline' do | ||||
|       it 'returns nil' do | ||||
|         allow(tracker).to receive(:file_extraction_pipeline?).and_return(false) | ||||
| 
 | ||||
|         expect(tracker.checksums).to be_nil | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#checksums_empty?' do | ||||
|     let(:tracker) { create(:bulk_import_tracker) } | ||||
| 
 | ||||
|     before do | ||||
|       allow(tracker).to receive_message_chain(:pipeline_class, :relation, :to_sym).and_return(:labels) | ||||
|     end | ||||
| 
 | ||||
|     context 'when checksums are missing' do | ||||
|       it 'returns true' do | ||||
|         allow(tracker).to receive(:checksums).and_return(nil) | ||||
| 
 | ||||
|         expect(tracker.checksums_empty?).to eq(true) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when checksums are present' do | ||||
|       it 'returns false' do | ||||
|         allow(tracker) | ||||
|           .to receive(:checksums) | ||||
|           .and_return({ labels: { source: 1, fetched: 1, imported: 1 } }) | ||||
| 
 | ||||
|         expect(tracker.checksums_empty?).to eq(false) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when checksums are all zeros' do | ||||
|       it 'returns true' do | ||||
|         allow(tracker) | ||||
|           .to receive(:checksums) | ||||
|           .and_return({ labels: { source: 0, fetched: 0, imported: 0 } }) | ||||
| 
 | ||||
|         expect(tracker.checksums_empty?).to eq(true) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'checksums persistence' do | ||||
|     let(:tracker) { create(:bulk_import_tracker, :started) } | ||||
| 
 | ||||
|     context 'when transitioned to finished' do | ||||
|       it 'persists the checksums' do | ||||
|         expect(BulkImports::ObjectCounter).to receive(:persist!).with(tracker) | ||||
| 
 | ||||
|         tracker.finish! | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when transitioned to failed' do | ||||
|       it 'persists the checksums' do | ||||
|         expect(BulkImports::ObjectCounter).to receive(:persist!).with(tracker) | ||||
| 
 | ||||
|         tracker.fail_op! | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -10,6 +10,38 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
|   let_it_be(:entity_2) { create(:bulk_import_entity, bulk_import: import_1) } | ||||
|   let_it_be(:entity_3) { create(:bulk_import_entity, bulk_import: import_2) } | ||||
|   let_it_be(:failure_3) { create(:bulk_import_failure, entity: entity_3) } | ||||
|   let_it_be(:tracker_1) do | ||||
|     create( | ||||
|       :bulk_import_tracker, | ||||
|       entity: entity_1, | ||||
|       relation: 'BulkImports::Common::Pipelines::LabelsPipeline', | ||||
|       source_objects_count: 3, | ||||
|       fetched_objects_count: 2, | ||||
|       imported_objects_count: 1 | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:tracker_2) do | ||||
|     create( | ||||
|       :bulk_import_tracker, | ||||
|       entity: entity_2, | ||||
|       relation: 'BulkImports::Common::Pipelines::MilestonesPipeline', | ||||
|       source_objects_count: 5, | ||||
|       fetched_objects_count: 4, | ||||
|       imported_objects_count: 3 | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:tracker_3) do | ||||
|     create( | ||||
|       :bulk_import_tracker, | ||||
|       entity: entity_3, | ||||
|       relation: 'BulkImports::Common::Pipelines::BoardsPipeline', | ||||
|       source_objects_count: 10, | ||||
|       fetched_objects_count: 9, | ||||
|       imported_objects_count: 8 | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     stub_application_setting(bulk_import_enabled: true) | ||||
|  | @ -370,6 +402,16 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
|       expect(json_response.pluck('id')).to contain_exactly(entity_1.id, entity_2.id, entity_3.id) | ||||
|     end | ||||
| 
 | ||||
|     it 'includes entity stats' do | ||||
|       request | ||||
| 
 | ||||
|       expect(json_response.pluck('stats')).to contain_exactly( | ||||
|         { 'labels' => { 'source' => 3, 'fetched' => 2, 'imported' => 1 } }, | ||||
|         { 'milestones' => { 'source' => 5, 'fetched' => 4, 'imported' => 3 } }, | ||||
|         { 'boards' => { 'source' => 10, 'fetched' => 9, 'imported' => 8 } } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'disabled feature' | ||||
|   end | ||||
| 
 | ||||
|  | @ -397,6 +439,14 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
|       expect(json_response.first['failures'].first['exception_message']).to eq(failure_3.exception_message) | ||||
|     end | ||||
| 
 | ||||
|     it 'includes entity stats' do | ||||
|       request | ||||
| 
 | ||||
|       expect(json_response.pluck('stats')).to contain_exactly( | ||||
|         { 'boards' => { 'source' => 10, 'fetched' => 9, 'imported' => 8 } } | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'disabled feature' | ||||
|   end | ||||
| 
 | ||||
|  | @ -410,6 +460,12 @@ RSpec.describe API::BulkImports, feature_category: :importers do | |||
|       expect(json_response['id']).to eq(entity_2.id) | ||||
|     end | ||||
| 
 | ||||
|     it 'includes entity stats' do | ||||
|       request | ||||
| 
 | ||||
|       expect(json_response['stats']).to eq({ 'milestones' => { 'source' => 5, 'fetched' => 4, 'imported' => 3 } }) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'disabled feature' | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ RSpec.describe BulkImports::RelationExportService, feature_category: :importers | |||
|       expect(export.batched?).to eq(false) | ||||
|       expect(export.batches_count).to eq(0) | ||||
|       expect(export.batches.count).to eq(0) | ||||
|       expect(export.total_objects_count).to eq(0) | ||||
|       expect(export.total_objects_count).to eq(1) | ||||
|     end | ||||
| 
 | ||||
|     it 'removes temp export files' do | ||||
|  |  | |||
|  | @ -105,6 +105,19 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'with a timecategory' do | ||||
|           let!(:timelog_category) { create(:timelog_category, name: 'bob', namespace: project.root_namespace) } | ||||
|           let(:note_text) { "a note \n/spend 1h [timecategory:bob]" } | ||||
| 
 | ||||
|           it 'sets the category of the new timelog' do | ||||
|             new_content, update_params = service.execute(note) | ||||
|             note.update!(note: new_content) | ||||
|             service.apply_updates(update_params, note) | ||||
| 
 | ||||
|             expect(Timelog.last.timelog_category_id).to eq(timelog_category.id) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'adds a system note' do | ||||
|           context 'when not specifying a date' do | ||||
|             let(:note_text) { "/spend 1h" } | ||||
|  |  | |||
|  | @ -3,6 +3,15 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_projects do | ||||
|   let_it_be(:group) { create(:group, :crm_enabled) } | ||||
|   let_it_be(:project) { create(:project, :public, group: group) } | ||||
|   let_it_be(:owner) { create(:user) } | ||||
|   let_it_be(:issue) { create(:issue, project: project, title: 'Issue 1') } | ||||
| 
 | ||||
|   before_all do | ||||
|     project.add_owner(owner) | ||||
|   end | ||||
| 
 | ||||
|   describe '#issues' do | ||||
|     describe 'confidential issues' do | ||||
|       let(:author) { create(:user) } | ||||
|  | @ -10,8 +19,6 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj | |||
|       let(:non_member) { create(:user) } | ||||
|       let(:member) { create(:user) } | ||||
|       let(:admin) { create(:admin) } | ||||
|       let(:project) { create(:project, :public) } | ||||
|       let!(:issue) { create(:issue, project: project, title: 'Issue 1') } | ||||
|       let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } | ||||
|       let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) } | ||||
| 
 | ||||
|  | @ -107,8 +114,6 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj | |||
| 
 | ||||
|   describe '#milestones' do | ||||
|     let(:user) { create(:user) } | ||||
|     let(:group) { create(:group) } | ||||
|     let(:project) { create(:project, group: group) } | ||||
|     let!(:group_milestone1) { create(:milestone, group: group, due_date: '2017-01-01', title: 'Second Title') } | ||||
|     let!(:group_milestone2) { create(:milestone, group: group, due_date: '2017-01-01', title: 'First Title') } | ||||
|     let!(:project_milestone) { create(:milestone, project: project, due_date: '2016-01-01') } | ||||
|  | @ -150,8 +155,6 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj | |||
| 
 | ||||
|   describe '#contacts' do | ||||
|     let_it_be(:user) { create(:user) } | ||||
|     let_it_be(:group) { create(:group, :crm_enabled) } | ||||
|     let_it_be(:project) { create(:project, group: group) } | ||||
|     let_it_be(:contact_1) { create(:contact, group: group) } | ||||
|     let_it_be(:contact_2) { create(:contact, group: group) } | ||||
|     let_it_be(:contact_3) { create(:contact, :inactive, group: group) } | ||||
|  | @ -252,4 +255,30 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#commands' do | ||||
|     subject(:commands) { described_class.new(project, owner).commands(issue) } | ||||
| 
 | ||||
|     context 'spend' do | ||||
|       it 'params include timecategory' do | ||||
|         expect(commands).to include(a_hash_including( | ||||
|           name: :spend, | ||||
|           params: ['time(1h30m | -1h30m) <date(YYYY-MM-DD)> <[timecategory:category-name]>'] | ||||
|         )) | ||||
|       end | ||||
| 
 | ||||
|       context 'when timelog_category_quick_action feature flag is disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(timelog_categories: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'params do not include timecategory' do | ||||
|           expect(commands).to include(a_hash_including( | ||||
|             name: :spend, | ||||
|             params: ['time(1h30m | -1h30m) <date(YYYY-MM-DD)>'] | ||||
|           )) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -384,6 +384,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning | |||
|           _, updates, _ = service.execute(content, issuable) | ||||
| 
 | ||||
|           expect(updates).to eq(spend_time: { | ||||
|                                   category: nil, | ||||
|                                   duration: 3600, | ||||
|                                   user_id: developer.id, | ||||
|                                   spent_at: DateTime.current | ||||
|  | @ -398,6 +399,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning | |||
|           _, updates, _ = service.execute(content, issuable) | ||||
| 
 | ||||
|           expect(updates).to eq(spend_time: { | ||||
|                                   category: nil, | ||||
|                                   duration: -7200, | ||||
|                                   user_id: developer.id, | ||||
|                                   spent_at: DateTime.current | ||||
|  | @ -417,6 +419,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning | |||
|         _, updates, _ = service.execute(content, issuable) | ||||
| 
 | ||||
|         expect(updates).to eq(spend_time: { | ||||
|                                 category: nil, | ||||
|                                 duration: 1800, | ||||
|                                 user_id: developer.id, | ||||
|                                 spent_at: Date.parse(date) | ||||
|  | @ -440,6 +443,14 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples 'spend command with category' do | ||||
|       it 'populates spend_time with expected attributes' do | ||||
|         _, updates, _ = service.execute(content, issuable) | ||||
| 
 | ||||
|         expect(updates).to match(spend_time: a_hash_including(category: 'pm')) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples 'remove_estimate command' do | ||||
|       it 'populates time_estimate: 0 if content contains /remove_estimate' do | ||||
|         _, updates, _ = service.execute(content, issuable) | ||||
|  | @ -1544,6 +1555,11 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning | |||
|       let(:issuable) { issue } | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'spend command with category' do | ||||
|       let(:content) { '/spent 30m [timecategory:pm]' } | ||||
|       let(:issuable) { issue } | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'failed command' do | ||||
|       let(:content) { '/spend' } | ||||
|       let(:issuable) { issue } | ||||
|  |  | |||
|  | @ -16,6 +16,8 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do | |||
|       allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| | ||||
|         allow(extractor).to receive(:extract).and_return(extracted_data) | ||||
|       end | ||||
| 
 | ||||
|       allow(subject).to receive(:set_source_objects_counter) | ||||
|     end | ||||
| 
 | ||||
|     context 'when wiki exists' do | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue