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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-20 15:52:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-20 15:52:10 +0300
commitdba864470fbcbb6bdd5b94eb510acdce62c962d8 (patch)
treee8ead0b84e7b814f5891d2c8cd3db2d6b635fb64 /lib
parentb7d29500f28ff59c8898cdf889a40d3da908f162 (diff)
Add latest changes from gitlab-org/gitlab@12-8-stable-ee
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb184
-rw-r--r--lib/api/applications.rb3
-rw-r--r--lib/api/award_emoji.rb2
-rw-r--r--lib/api/broadcast_messages.rb11
-rw-r--r--lib/api/commit_statuses.rb14
-rw-r--r--lib/api/commits.rb5
-rw-r--r--lib/api/discussions.rb18
-rw-r--r--lib/api/entities.rb1966
-rw-r--r--lib/api/entities/access_requester.rb10
-rw-r--r--lib/api/entities/appearance.rb29
-rw-r--r--lib/api/entities/application.rb13
-rw-r--r--lib/api/entities/application_setting.rb38
-rw-r--r--lib/api/entities/application_statistics.rb54
-rw-r--r--lib/api/entities/application_with_secret.rb10
-rw-r--r--lib/api/entities/avatar.rb11
-rw-r--r--lib/api/entities/award_emoji.rb13
-rw-r--r--lib/api/entities/badge.rb12
-rw-r--r--lib/api/entities/basic_badge_details.rb17
-rw-r--r--lib/api/entities/basic_group_details.rb11
-rw-r--r--lib/api/entities/basic_project_details.rb55
-rw-r--r--lib/api/entities/basic_ref.rb9
-rw-r--r--lib/api/entities/blame_range.rb10
-rw-r--r--lib/api/entities/blame_range_commit.rb13
-rw-r--r--lib/api/entities/blob.rb20
-rw-r--r--lib/api/entities/board.rb16
-rw-r--r--lib/api/entities/branch.rb41
-rw-r--r--lib/api/entities/broadcast_message.rb10
-rw-r--r--lib/api/entities/cluster.rb14
-rw-r--r--lib/api/entities/cluster_group.rb9
-rw-r--r--lib/api/entities/cluster_project.rb9
-rw-r--r--lib/api/entities/commit.rb14
-rw-r--r--lib/api/entities/commit_detail.rb22
-rw-r--r--lib/api/entities/commit_note.rb14
-rw-r--r--lib/api/entities/commit_signature.rb12
-rw-r--r--lib/api/entities/commit_stats.rb9
-rw-r--r--lib/api/entities/commit_status.rb11
-rw-r--r--lib/api/entities/commit_with_stats.rb9
-rw-r--r--lib/api/entities/compare.rb25
-rw-r--r--lib/api/entities/container_expiration_policy.rb14
-rw-r--r--lib/api/entities/contributor.rb9
-rw-r--r--lib/api/entities/custom_attribute.rb10
-rw-r--r--lib/api/entities/deploy_key_with_user.rb9
-rw-r--r--lib/api/entities/deploy_keys_project.rb10
-rw-r--r--lib/api/entities/deployment.rb13
-rw-r--r--lib/api/entities/diff.rb13
-rw-r--r--lib/api/entities/diff_position.rb10
-rw-r--r--lib/api/entities/diff_refs.rb9
-rw-r--r--lib/api/entities/discussion.rb11
-rw-r--r--lib/api/entities/email.rb9
-rw-r--r--lib/api/entities/environment.rb11
-rw-r--r--lib/api/entities/environment_basic.rb9
-rw-r--r--lib/api/entities/event.rb23
-rw-r--r--lib/api/entities/external_issue.rb10
-rw-r--r--lib/api/entities/feature.rb22
-rw-r--r--lib/api/entities/feature_gate.rb10
-rw-r--r--lib/api/entities/global_notification_setting.rb11
-rw-r--r--lib/api/entities/gpg_key.rb9
-rw-r--r--lib/api/entities/group.rb38
-rw-r--r--lib/api/entities/group_access.rb8
-rw-r--r--lib/api/entities/group_detail.rb38
-rw-r--r--lib/api/entities/group_label.rb8
-rw-r--r--lib/api/entities/hook.rb10
-rw-r--r--lib/api/entities/identity.rb11
-rw-r--r--lib/api/entities/impersonation_token.rb9
-rw-r--r--lib/api/entities/impersonation_token_with_token.rb9
-rw-r--r--lib/api/entities/internal_post_receive/message.rb12
-rw-r--r--lib/api/entities/internal_post_receive/response.rb12
-rw-r--r--lib/api/entities/issuable_entity.rb18
-rw-r--r--lib/api/entities/issuable_references.rb19
-rw-r--r--lib/api/entities/issuable_time_stats.rb26
-rw-r--r--lib/api/entities/issue.rb50
-rw-r--r--lib/api/entities/issue_basic.rb45
-rw-r--r--lib/api/entities/job.rb13
-rw-r--r--lib/api/entities/job_artifact.rb9
-rw-r--r--lib/api/entities/job_artifact_file.rb10
-rw-r--r--lib/api/entities/job_basic.rb18
-rw-r--r--lib/api/entities/job_basic_with_project.rb9
-rw-r--r--lib/api/entities/job_request/artifacts.rb17
-rw-r--r--lib/api/entities/job_request/cache.rb11
-rw-r--r--lib/api/entities/job_request/credentials.rb11
-rw-r--r--lib/api/entities/job_request/dependency.rb12
-rw-r--r--lib/api/entities/job_request/git_info.rb14
-rw-r--r--lib/api/entities/job_request/image.rb12
-rw-r--r--lib/api/entities/job_request/job_info.rb12
-rw-r--r--lib/api/entities/job_request/port.rb11
-rw-r--r--lib/api/entities/job_request/response.rb35
-rw-r--r--lib/api/entities/job_request/runner_info.rb12
-rw-r--r--lib/api/entities/job_request/service.rb11
-rw-r--r--lib/api/entities/job_request/step.rb11
-rw-r--r--lib/api/entities/label.rb25
-rw-r--r--lib/api/entities/label_basic.rb9
-rw-r--r--lib/api/entities/license.rb14
-rw-r--r--lib/api/entities/license_basic.rb11
-rw-r--r--lib/api/entities/list.rb13
-rw-r--r--lib/api/entities/member.rb13
-rw-r--r--lib/api/entities/member_access.rb14
-rw-r--r--lib/api/entities/membership.rb14
-rw-r--r--lib/api/entities/merge_request.rb53
-rw-r--r--lib/api/entities/merge_request_basic.rb96
-rw-r--r--lib/api/entities/merge_request_changes.rb11
-rw-r--r--lib/api/entities/merge_request_diff.rb10
-rw-r--r--lib/api/entities/merge_request_diff_full.rb13
-rw-r--r--lib/api/entities/merge_request_simple.rb12
-rw-r--r--lib/api/entities/milestone.rb19
-rw-r--r--lib/api/entities/mr_note.rb10
-rw-r--r--lib/api/entities/namespace.rb17
-rw-r--r--lib/api/entities/namespace_basic.rb17
-rw-r--r--lib/api/entities/note.rb30
-rw-r--r--lib/api/entities/notification_setting.rb14
-rw-r--r--lib/api/entities/pages_domain.rb20
-rw-r--r--lib/api/entities/pages_domain_basic.rb22
-rw-r--r--lib/api/entities/pages_domain_certificate.rb12
-rw-r--r--lib/api/entities/pages_domain_certificate_expiration.rb10
-rw-r--r--lib/api/entities/personal_access_token.rb13
-rw-r--r--lib/api/entities/personal_access_token_with_token.rb9
-rw-r--r--lib/api/entities/personal_snippet.rb11
-rw-r--r--lib/api/entities/pipeline.rb17
-rw-r--r--lib/api/entities/pipeline_basic.rb14
-rw-r--r--lib/api/entities/pipeline_schedule.rb12
-rw-r--r--lib/api/entities/pipeline_schedule_details.rb10
-rw-r--r--lib/api/entities/platform/kubernetes.rb14
-rw-r--r--lib/api/entities/project.rb134
-rw-r--r--lib/api/entities/project_access.rb8
-rw-r--r--lib/api/entities/project_daily_fetches.rb10
-rw-r--r--lib/api/entities/project_daily_statistics.rb12
-rw-r--r--lib/api/entities/project_export_status.rb20
-rw-r--r--lib/api/entities/project_group_link.rb9
-rw-r--r--lib/api/entities/project_hook.rb12
-rw-r--r--lib/api/entities/project_identity.rb12
-rw-r--r--lib/api/entities/project_import_status.rb14
-rw-r--r--lib/api/entities/project_label.rb14
-rw-r--r--lib/api/entities/project_service.rb17
-rw-r--r--lib/api/entities/project_service_basic.rb17
-rw-r--r--lib/api/entities/project_snippet.rb8
-rw-r--r--lib/api/entities/project_statistics.rb14
-rw-r--r--lib/api/entities/project_with_access.rb47
-rw-r--r--lib/api/entities/protected_branch.rb14
-rw-r--r--lib/api/entities/protected_ref_access.rb14
-rw-r--r--lib/api/entities/protected_tag.rb10
-rw-r--r--lib/api/entities/provider/gcp.rb17
-rw-r--r--lib/api/entities/push_event_payload.rb10
-rw-r--r--lib/api/entities/release.rb54
-rw-r--r--lib/api/entities/releases/link.rb14
-rw-r--r--lib/api/entities/releases/source.rb12
-rw-r--r--lib/api/entities/remote_mirror.rb17
-rw-r--r--lib/api/entities/resource_label_event.rb19
-rw-r--r--lib/api/entities/runner.rb16
-rw-r--r--lib/api/entities/runner_details.rb34
-rw-r--r--lib/api/entities/runner_registration_details.rb9
-rw-r--r--lib/api/entities/shared_group.rb17
-rw-r--r--lib/api/entities/snippet.rb15
-rw-r--r--lib/api/entities/ssh_key.rb9
-rw-r--r--lib/api/entities/ssh_key_with_user.rb9
-rw-r--r--lib/api/entities/suggestion.rb15
-rw-r--r--lib/api/entities/tag.rb23
-rw-r--r--lib/api/entities/tag_release.rb11
-rw-r--r--lib/api/entities/template.rb9
-rw-r--r--lib/api/entities/templates_list.rb10
-rw-r--r--lib/api/entities/todo.rb48
-rw-r--r--lib/api/entities/tree_object.rb15
-rw-r--r--lib/api/entities/trigger.rb15
-rw-r--r--lib/api/entities/user.rb10
-rw-r--r--lib/api/entities/user_activity.rb11
-rw-r--r--lib/api/entities/user_agent_detail.rb11
-rw-r--r--lib/api/entities/user_basic.rb20
-rw-r--r--lib/api/entities/user_details_with_admin.rb11
-rw-r--r--lib/api/entities/user_public.rb21
-rw-r--r--lib/api/entities/user_safe.rb9
-rw-r--r--lib/api/entities/user_stars_project.rb10
-rw-r--r--lib/api/entities/user_status.rb13
-rw-r--r--lib/api/entities/user_with_admin.rb11
-rw-r--r--lib/api/entities/variable.rb12
-rw-r--r--lib/api/entities/wiki_attachment.rb27
-rw-r--r--lib/api/entities/wiki_page.rb9
-rw-r--r--lib/api/entities/wiki_page_basic.rb11
-rw-r--r--lib/api/error_tracking.rb28
-rw-r--r--lib/api/group_boards.rb4
-rw-r--r--lib/api/group_clusters.rb4
-rw-r--r--lib/api/group_export.rb4
-rw-r--r--lib/api/group_import.rb88
-rw-r--r--lib/api/groups.rb16
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/helpers/file_upload_helpers.rb15
-rw-r--r--lib/api/helpers/internal_helpers.rb40
-rw-r--r--lib/api/helpers/members_helpers.rb2
-rw-r--r--lib/api/helpers/notes_helpers.rb20
-rw-r--r--lib/api/helpers/pagination_strategies.rb2
-rw-r--r--lib/api/helpers/projects_helpers.rb6
-rw-r--r--lib/api/helpers/runner.rb6
-rw-r--r--lib/api/helpers/services_helpers.rb8
-rw-r--r--lib/api/internal/base.rb38
-rw-r--r--lib/api/issues.rb1
-rw-r--r--lib/api/keys.rb2
-rw-r--r--lib/api/lsif_data.rb38
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_requests.rb72
-rw-r--r--lib/api/notes.rb10
-rw-r--r--lib/api/pipeline_schedules.rb19
-rw-r--r--lib/api/project_clusters.rb4
-rw-r--r--lib/api/project_container_repositories.rb6
-rw-r--r--lib/api/project_import.rb17
-rw-r--r--lib/api/project_snippets.rb6
-rw-r--r--lib/api/protected_branches.rb7
-rw-r--r--lib/api/releases.rb11
-rw-r--r--lib/api/repositories.rb9
-rw-r--r--lib/api/resource_label_events.rb5
-rw-r--r--lib/api/runner.rb32
-rw-r--r--lib/api/search.rb10
-rw-r--r--lib/api/snippets.rb4
-rw-r--r--lib/api/users.rb59
-rw-r--r--lib/backup/manager.rb6
-rw-r--r--lib/backup/repository.rb1
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb36
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb2
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb2
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb2
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/project_reference_filter.rb2
-rw-r--r--lib/banzai/filter/repository_link_filter.rb9
-rw-r--r--lib/banzai/filter/table_of_contents_tag_filter.rb45
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/snippet_parser.rb2
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/container_registry/client.rb33
-rw-r--r--lib/container_registry/tag.rb2
-rw-r--r--lib/declarative_policy/runner.rb1
-rw-r--r--lib/feature.rb2
-rw-r--r--lib/feature/gitaly.rb24
-rw-r--r--lib/gitaly/server.rb32
-rw-r--r--lib/gitlab/action_view_output/context.rb41
-rw-r--r--lib/gitlab/alerting/alert.rb167
-rw-r--r--lib/gitlab/alerting/alert_annotation.rb11
-rw-r--r--lib/gitlab/alerting/notification_payload_parser.rb75
-rw-r--r--lib/gitlab/analytics/cycle_analytics/default_stages.rb2
-rw-r--r--lib/gitlab/application_context.rb14
-rw-r--r--lib/gitlab/application_rate_limiter.rb1
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/auth/current_user_mode.rb61
-rw-r--r--lib/gitlab/auth/ldap/access.rb2
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb2
-rw-r--r--lib/gitlab/background_migration.rb8
-rw-r--r--lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb52
-rw-r--r--lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb4
-rw-r--r--lib/gitlab/background_migration/backfill_project_repositories.rb4
-rw-r--r--lib/gitlab/background_migration/backfill_project_settings.rb18
-rw-r--r--lib/gitlab/background_migration/fix_orphan_promoted_issues.rb13
-rw-r--r--lib/gitlab/background_migration/fix_projects_without_project_feature.rb72
-rw-r--r--lib/gitlab/background_migration/link_lfs_objects.rb31
-rw-r--r--lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb21
-rw-r--r--lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb39
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb95
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb24
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/epic.rb51
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb18
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/note.rb61
-rw-r--r--lib/gitlab/batch_worker_context.rb32
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb4
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/build/rules.rb8
-rw-r--r--lib/gitlab/ci/config/entry/bridge.rb152
-rw-r--r--lib/gitlab/ci/config/entry/job.rb3
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb4
-rw-r--r--lib/gitlab/ci/config/entry/needs.rb6
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb5
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb93
-rw-r--r--lib/gitlab/ci/config/external/file/template.rb2
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb5
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb16
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb11
-rw-r--r--lib/gitlab/ci/pipeline/seed/deployment.rb10
-rw-r--r--lib/gitlab/ci/reports/test_reports_comparer.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite_comparer.rb30
-rw-r--r--lib/gitlab/ci/status/build/failed.rb8
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml33
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/yaml_processor.rb1
-rw-r--r--lib/gitlab/color_schemes.rb4
-rw-r--r--lib/gitlab/content_disposition.rb54
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb2
-rw-r--r--lib/gitlab/data_builder/build.rb6
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/database.rb9
-rw-r--r--lib/gitlab/database/batch_count.rb89
-rw-r--r--lib/gitlab/database/migration_helpers.rb60
-rw-r--r--lib/gitlab/database/sha_attribute.rb9
-rw-r--r--lib/gitlab/database/subquery.rb16
-rw-r--r--lib/gitlab/database/with_lock_retries.rb158
-rw-r--r--lib/gitlab/database/x509_serial_number_attribute.rb26
-rw-r--r--lib/gitlab/database_importers/common_metrics.rb2
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb9
-rw-r--r--lib/gitlab/database_importers/self_monitoring/helpers.rb4
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb29
-rw-r--r--lib/gitlab/dependency_linker/godeps_json_linker.rb6
-rw-r--r--lib/gitlab/diff/deprecated_highlight_cache.rb68
-rw-r--r--lib/gitlab/diff/diff_refs.rb2
-rw-r--r--lib/gitlab/diff/file.rb14
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb6
-rw-r--r--lib/gitlab/diff/formatters/image_formatter.rb6
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb2
-rw-r--r--lib/gitlab/diff/highlight_cache.rb21
-rw-r--r--lib/gitlab/diff/suggestion_diff.rb4
-rw-r--r--lib/gitlab/email/attachment_uploader.rb18
-rw-r--r--lib/gitlab/email/hook/smime_signature_interceptor.rb1
-rw-r--r--lib/gitlab/email/smime/certificate.rb6
-rw-r--r--lib/gitlab/email/smime/signer.rb10
-rw-r--r--lib/gitlab/error_tracking.rb7
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb2
-rw-r--r--lib/gitlab/error_tracking/error.rb5
-rw-r--r--lib/gitlab/error_tracking/error_collection.rb23
-rw-r--r--lib/gitlab/error_tracking/error_event.rb6
-rw-r--r--lib/gitlab/etag_caching/middleware.rb21
-rw-r--r--lib/gitlab/experimentation.rb27
-rw-r--r--lib/gitlab/file_finder.rb9
-rw-r--r--lib/gitlab/file_hook.rb2
-rw-r--r--lib/gitlab/git/blob.rb24
-rw-r--r--lib/gitlab/git/commit.rb3
-rw-r--r--lib/gitlab/git/diff_collection.rb2
-rw-r--r--lib/gitlab/git/repository.rb25
-rw-r--r--lib/gitlab/git/rugged_impl/use_rugged.rb4
-rw-r--r--lib/gitlab/git_access.rb11
-rw-r--r--lib/gitlab/git_access_snippet.rb48
-rw-r--r--lib/gitlab/gitaly_client.rb13
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb1
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb30
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb34
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb12
-rw-r--r--lib/gitlab/gitaly_client/server_service.rb18
-rw-r--r--lib/gitlab/gl_repository.rb21
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb47
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/gpg.rb7
-rw-r--r--lib/gitlab/gpg/commit.rb31
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb9
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb33
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb10
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb6
-rw-r--r--lib/gitlab/graphql/connections/keyset/order_info.rb33
-rw-r--r--lib/gitlab/graphql/connections/keyset/query_builder.rb11
-rw-r--r--lib/gitlab/graphql/docs/helper.rb4
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml6
-rw-r--r--lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb12
-rw-r--r--lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb2
-rw-r--r--lib/gitlab/health_checks/base_abstract_check.rb8
-rw-r--r--lib/gitlab/highlight.rb2
-rw-r--r--lib/gitlab/import_export/base_relation_factory.rb3
-rw-r--r--lib/gitlab/import_export/group_import_export.yml44
-rw-r--r--lib/gitlab/import_export/group_object_builder.rb55
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb6
-rw-r--r--lib/gitlab/import_export/group_relation_factory.rb40
-rw-r--r--lib/gitlab/import_export/group_tree_restorer.rb116
-rw-r--r--lib/gitlab/import_export/import_export.yml11
-rw-r--r--lib/gitlab/import_export/import_failure_service.rb12
-rw-r--r--lib/gitlab/import_export/members_mapper.rb12
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb21
-rw-r--r--lib/gitlab/import_export/project_tree_loader.rb72
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb23
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb17
-rw-r--r--lib/gitlab/incoming_email.rb2
-rw-r--r--lib/gitlab/jira/http_client.rb2
-rw-r--r--lib/gitlab/kubernetes/generic_secret.rb36
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb31
-rw-r--r--lib/gitlab/kubernetes/tls_secret.rb44
-rw-r--r--lib/gitlab/log_timestamp_formatter.rb11
-rw-r--r--lib/gitlab/looping_batcher.rb99
-rw-r--r--lib/gitlab/mail_room.rb62
-rw-r--r--lib/gitlab/marginalia.rb8
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb16
-rw-r--r--lib/gitlab/metrics/dashboard/service_selector.rb54
-rw-r--r--lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb40
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb71
-rw-r--r--lib/gitlab/middleware/go.rb2
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb20
-rw-r--r--lib/gitlab/patch/active_record_query_cache.rb39
-rw-r--r--lib/gitlab/phabricator_import/base_worker.rb82
-rw-r--r--lib/gitlab/phabricator_import/import_tasks_worker.rb10
-rw-r--r--lib/gitlab/profiler/total_time_flat_printer.rb5
-rw-r--r--lib/gitlab/project_search_results.rb28
-rw-r--r--lib/gitlab/project_transfer.rb2
-rw-r--r--lib/gitlab/prometheus/queries/knative_invocation_query.rb2
-rw-r--r--lib/gitlab/query_limiting/middleware.rb4
-rw-r--r--lib/gitlab/quick_actions/extractor.rb53
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb20
-rw-r--r--lib/gitlab/quick_actions/substitution_definition.rb4
-rw-r--r--lib/gitlab/redis/boolean.rb109
-rw-r--r--lib/gitlab/regex.rb3
-rw-r--r--lib/gitlab/repo_path.rb14
-rw-r--r--lib/gitlab/repository_cache.rb4
-rw-r--r--lib/gitlab/repository_cache_adapter.rb10
-rw-r--r--lib/gitlab/repository_hash_cache.rb176
-rw-r--r--lib/gitlab/request_context.rb1
-rw-r--r--lib/gitlab/runtime.rb45
-rw-r--r--lib/gitlab/search/found_blob.rb1
-rw-r--r--lib/gitlab/search/found_wiki_page.rb25
-rw-r--r--lib/gitlab/serverless/domain.rb13
-rw-r--r--lib/gitlab/serverless/function_uri.rb46
-rw-r--r--lib/gitlab/serverless/service.rb98
-rw-r--r--lib/gitlab/shell.rb159
-rw-r--r--lib/gitlab/sidekiq_config.rb79
-rw-r--r--lib/gitlab/sidekiq_config/cli_methods.rb86
-rw-r--r--lib/gitlab/sidekiq_config/dummy_worker.rb33
-rw-r--r--lib/gitlab/sidekiq_config/worker.rb67
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb23
-rw-r--r--lib/gitlab/sidekiq_middleware.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/admin_mode/client.rb34
-rw-r--r--lib/gitlab/sidekiq_middleware/admin_mode/server.rb24
-rw-r--r--lib/gitlab/sidekiq_middleware/client_metrics.rb6
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context.rb15
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/client.rb23
-rw-r--r--lib/gitlab/sidekiq_middleware/worker_context/server.rb21
-rw-r--r--lib/gitlab/signed_commit.rb34
-rw-r--r--lib/gitlab/tab_width.rb29
-rw-r--r--lib/gitlab/template/base_template.rb2
-rw-r--r--lib/gitlab/themes.rb4
-rw-r--r--lib/gitlab/usage_data.rb33
-rw-r--r--lib/gitlab/utils/deep_size.rb4
-rw-r--r--lib/gitlab/utils/log_limited_array.rb27
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--lib/gitlab/x509/commit.rb199
-rw-r--r--lib/gitlab_danger.rb1
-rw-r--r--lib/microsoft_teams/notifier.rb5
-rw-r--r--lib/quality/kubernetes_client.rb2
-rw-r--r--lib/quality/test_level.rb2
-rw-r--r--lib/rspec_flaky/report.rb2
-rw-r--r--lib/sentry/client.rb6
-rw-r--r--lib/sentry/client/issue_link.rb35
-rwxr-xr-xlib/support/init.d/gitlab4
-rw-r--r--lib/system_check/helpers.rb6
-rw-r--r--lib/tasks/cache.rake2
-rw-r--r--lib/tasks/ci/cleanup.rake2
-rw-r--r--lib/tasks/dev.rake4
-rw-r--r--lib/tasks/gitlab/artifacts/migrate.rake2
-rw-r--r--lib/tasks/gitlab/assets.rake1
-rw-r--r--lib/tasks/gitlab/backup.rake20
-rw-r--r--lib/tasks/gitlab/bulk_add_permission.rake8
-rw-r--r--lib/tasks/gitlab/check.rake16
-rw-r--r--lib/tasks/gitlab/db.rake10
-rw-r--r--lib/tasks/gitlab/exclusive_lease.rake2
-rw-r--r--lib/tasks/gitlab/gitaly.rake2
-rw-r--r--lib/tasks/gitlab/graphql.rake6
-rw-r--r--lib/tasks/gitlab/import.rake2
-rw-r--r--lib/tasks/gitlab/import_export.rake6
-rw-r--r--lib/tasks/gitlab/import_export/import.rake80
-rw-r--r--lib/tasks/gitlab/info.rake2
-rw-r--r--lib/tasks/gitlab/lfs/migrate.rake2
-rw-r--r--lib/tasks/gitlab/metrics.rake2
-rw-r--r--lib/tasks/gitlab/seed/group_seed.rake233
-rw-r--r--lib/tasks/gitlab/shell.rake6
-rw-r--r--lib/tasks/gitlab/sidekiq.rake100
-rw-r--r--lib/tasks/gitlab/two_factor.rake6
-rw-r--r--lib/tasks/gitlab/web_hook.rake6
-rw-r--r--lib/tasks/gitlab/workhorse.rake2
-rw-r--r--lib/tasks/import.rake2
-rw-r--r--lib/tasks/lint.rake19
-rw-r--r--lib/tasks/migrate/composite_primary_keys.rake4
-rw-r--r--lib/tasks/pngquant.rake4
-rw-r--r--lib/tasks/sidekiq.rake8
-rw-r--r--lib/tasks/yarn.rake1
473 files changed, 8291 insertions, 3424 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 1aee4fd30ee..9a1e0e3f8e9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -47,7 +47,8 @@ module API
Gitlab::ApplicationContext.push(
user: -> { current_user },
project: -> { @project },
- namespace: -> { @group }
+ namespace: -> { @group },
+ caller_id: route.origin
)
end
@@ -102,94 +103,103 @@ module API
helpers ::API::Helpers
helpers ::API::Helpers::CommonHelpers
- # Keep in alphabetical order
- mount ::API::AccessRequests
- mount ::API::Appearance
- mount ::API::Applications
- mount ::API::Avatar
- mount ::API::AwardEmoji
- mount ::API::Badges
- mount ::API::Boards
- mount ::API::Branches
- mount ::API::BroadcastMessages
- mount ::API::Commits
- mount ::API::CommitStatuses
- mount ::API::DeployKeys
- mount ::API::Deployments
- mount ::API::Environments
- mount ::API::ErrorTracking
- mount ::API::Events
- mount ::API::Features
- mount ::API::Files
- mount ::API::GroupBoards
- mount ::API::GroupClusters
- mount ::API::GroupExport
- mount ::API::GroupLabels
- mount ::API::GroupMilestones
- mount ::API::Groups
- mount ::API::GroupContainerRepositories
- mount ::API::GroupVariables
- mount ::API::ImportGithub
+ namespace do
+ after do
+ ::Users::ActivityService.new(@current_user).execute if Feature.enabled?(:api_activity_logging)
+ end
+
+ # Keep in alphabetical order
+ mount ::API::AccessRequests
+ mount ::API::Appearance
+ mount ::API::Applications
+ mount ::API::Avatar
+ mount ::API::AwardEmoji
+ mount ::API::Badges
+ mount ::API::Boards
+ mount ::API::Branches
+ mount ::API::BroadcastMessages
+ mount ::API::Commits
+ mount ::API::CommitStatuses
+ mount ::API::DeployKeys
+ mount ::API::Deployments
+ mount ::API::Environments
+ mount ::API::ErrorTracking
+ mount ::API::Events
+ mount ::API::Features
+ mount ::API::Files
+ mount ::API::GroupBoards
+ mount ::API::GroupClusters
+ mount ::API::GroupExport
+ mount ::API::GroupImport
+ mount ::API::GroupLabels
+ mount ::API::GroupMilestones
+ mount ::API::Groups
+ mount ::API::GroupContainerRepositories
+ mount ::API::GroupVariables
+ mount ::API::ImportGithub
+ mount ::API::Issues
+ mount ::API::JobArtifacts
+ mount ::API::Jobs
+ mount ::API::Keys
+ mount ::API::Labels
+ mount ::API::Lint
+ mount ::API::LsifData
+ mount ::API::Markdown
+ mount ::API::Members
+ mount ::API::MergeRequestDiffs
+ mount ::API::MergeRequests
+ mount ::API::Namespaces
+ mount ::API::Notes
+ mount ::API::Discussions
+ mount ::API::ResourceLabelEvents
+ mount ::API::NotificationSettings
+ mount ::API::Pages
+ mount ::API::PagesDomains
+ mount ::API::Pipelines
+ mount ::API::PipelineSchedules
+ mount ::API::ProjectClusters
+ mount ::API::ProjectContainerRepositories
+ mount ::API::ProjectEvents
+ mount ::API::ProjectExport
+ mount ::API::ProjectImport
+ mount ::API::ProjectHooks
+ mount ::API::ProjectMilestones
+ mount ::API::Projects
+ mount ::API::ProjectSnapshots
+ mount ::API::ProjectSnippets
+ mount ::API::ProjectStatistics
+ mount ::API::ProjectTemplates
+ mount ::API::ProtectedBranches
+ mount ::API::ProtectedTags
+ mount ::API::Releases
+ mount ::API::Release::Links
+ mount ::API::RemoteMirrors
+ mount ::API::Repositories
+ mount ::API::Runner
+ mount ::API::Runners
+ mount ::API::Search
+ mount ::API::Services
+ mount ::API::Settings
+ mount ::API::SidekiqMetrics
+ mount ::API::Snippets
+ mount ::API::Statistics
+ mount ::API::Submodules
+ mount ::API::Subscriptions
+ mount ::API::Suggestions
+ mount ::API::SystemHooks
+ mount ::API::Tags
+ mount ::API::Templates
+ mount ::API::Todos
+ mount ::API::Triggers
+ mount ::API::UserCounts
+ mount ::API::Users
+ mount ::API::Variables
+ mount ::API::Version
+ mount ::API::Wikis
+ end
+
mount ::API::Internal::Base
mount ::API::Internal::Pages
- mount ::API::Issues
- mount ::API::JobArtifacts
- mount ::API::Jobs
- mount ::API::Keys
- mount ::API::Labels
- mount ::API::Lint
- mount ::API::Markdown
- mount ::API::Members
- mount ::API::MergeRequestDiffs
- mount ::API::MergeRequests
- mount ::API::Namespaces
- mount ::API::Notes
- mount ::API::Discussions
- mount ::API::ResourceLabelEvents
- mount ::API::NotificationSettings
- mount ::API::Pages
- mount ::API::PagesDomains
- mount ::API::Pipelines
- mount ::API::PipelineSchedules
- mount ::API::ProjectClusters
- mount ::API::ProjectContainerRepositories
- mount ::API::ProjectEvents
- mount ::API::ProjectExport
- mount ::API::ProjectImport
- mount ::API::ProjectHooks
- mount ::API::ProjectMilestones
- mount ::API::Projects
- mount ::API::ProjectSnapshots
- mount ::API::ProjectSnippets
- mount ::API::ProjectStatistics
- mount ::API::ProjectTemplates
- mount ::API::ProtectedBranches
- mount ::API::ProtectedTags
- mount ::API::Releases
- mount ::API::Release::Links
- mount ::API::RemoteMirrors
- mount ::API::Repositories
- mount ::API::Runner
- mount ::API::Runners
- mount ::API::Search
- mount ::API::Services
- mount ::API::Settings
- mount ::API::SidekiqMetrics
- mount ::API::Snippets
- mount ::API::Statistics
- mount ::API::Submodules
- mount ::API::Subscriptions
- mount ::API::Suggestions
- mount ::API::SystemHooks
- mount ::API::Tags
- mount ::API::Templates
- mount ::API::Todos
- mount ::API::Triggers
- mount ::API::UserCounts
- mount ::API::Users
- mount ::API::Variables
- mount ::API::Version
- mount ::API::Wikis
route :any, '*path' do
error!('404 Not Found', 404)
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 4e9843e17e8..70e6b8395d7 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -14,6 +14,9 @@ module API
requires :name, type: String, desc: 'Application name'
requires :redirect_uri, type: String, desc: 'Application redirect URI'
requires :scopes, type: String, desc: 'Application scopes'
+
+ optional :confidential, type: Boolean, default: true,
+ desc: 'Application will be used where the client secret is confidential'
end
post do
application = Doorkeeper::Application.new(declared_params)
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 7a815fa3dde..8e3b3ff8ce5 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -127,6 +127,8 @@ module API
case awardable
when Note
read_ability(awardable.noteable)
+ when Snippet, ProjectSnippet
+ :read_snippet
else
:"read_#{awardable.class.to_s.underscore}"
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 994e12445b7..af7c69f857e 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -4,9 +4,6 @@ module API
class BroadcastMessages < Grape::API
include PaginationParams
- before { authenticate! }
- before { authenticated_as_admin! }
-
resource :broadcast_messages do
helpers do
def find_message
@@ -38,8 +35,11 @@ module API
optional :color, type: String, desc: 'Background color'
optional :font, type: String, desc: 'Foreground color'
optional :target_path, type: String, desc: 'Target path'
+ optional :broadcast_type, type: String, values: BroadcastMessage.broadcast_types.keys, desc: 'Broadcast type. Defaults to banner', default: -> { 'banner' }
end
post do
+ authenticated_as_admin!
+
message = BroadcastMessage.create(declared_params(include_missing: false))
if message.persisted?
@@ -74,8 +74,11 @@ module API
optional :color, type: String, desc: 'Background color'
optional :font, type: String, desc: 'Foreground color'
optional :target_path, type: String, desc: 'Target path'
+ optional :broadcast_type, type: String, values: BroadcastMessage.broadcast_types.keys, desc: 'Broadcast Type'
end
put ':id' do
+ authenticated_as_admin!
+
message = find_message
if message.update(declared_params(include_missing: false))
@@ -93,6 +96,8 @@ module API
requires :id, type: Integer, desc: 'Broadcast message ID'
end
delete ':id' do
+ authenticated_as_admin!
+
message = find_message
destroy_conditionally!(message)
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 569c4e04dc5..b4c5d7869a2 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -76,14 +76,12 @@ module API
name = params[:name] || params[:context] || 'default'
- unless pipeline
- pipeline = user_project.ci_pipelines.create!(
- source: :external,
- sha: commit.sha,
- ref: ref,
- user: current_user,
- protected: user_project.protected_for?(ref))
- end
+ pipeline ||= user_project.ci_pipelines.create!(
+ source: :external,
+ sha: commit.sha,
+ ref: ref,
+ user: current_user,
+ protected: user_project.protected_for?(ref))
authorize! :update_pipeline, pipeline
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 9dcf9b015aa..dfb0066ceb0 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -38,6 +38,7 @@ module API
optional :all, type: Boolean, desc: 'Every commit will be returned'
optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
+ optional :order, type: String, desc: 'List commits in order', default: 'default', values: %w[default topo]
use :pagination
end
get ':id/repository/commits' do
@@ -49,6 +50,7 @@ module API
all = params[:all]
with_stats = params[:with_stats]
first_parent = params[:first_parent]
+ order = params[:order]
commits = user_project.repository.commits(ref,
path: path,
@@ -57,7 +59,8 @@ module API
before: before,
after: after,
all: all,
- first_parent: first_parent)
+ first_parent: first_parent,
+ order: order)
commit_count =
if all || path || before || after || first_parent
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 9125207167c..25d38615c7f 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -26,7 +26,7 @@ module API
end
get ":id/#{noteables_path}/:noteable_id/discussions" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable)
discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
@@ -42,7 +42,7 @@ module API
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty?
@@ -77,7 +77,7 @@ module API
end
end
post ":id/#{noteables_path}/:noteable_id/discussions" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
type = params[:position] ? 'DiffNote' : 'DiscussionNote'
id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id
@@ -107,7 +107,7 @@ module API
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty?
@@ -127,7 +127,7 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
first_note = notes.first
@@ -161,7 +161,7 @@ module API
requires :note_id, type: Integer, desc: 'The ID of a note'
end
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
get_note(noteable, params[:note_id])
end
@@ -178,7 +178,7 @@ module API
exactly_one_of :body, :resolved
end
put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
if params[:resolved].nil?
update_note(noteable, params[:note_id])
@@ -196,7 +196,7 @@ module API
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
@@ -211,7 +211,7 @@ module API
requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
end
put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
resolve_discussion(noteable, params[:discussion_id], params[:resolved])
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
deleted file mode 100644
index 8c4d986eb34..00000000000
--- a/lib/api/entities.rb
+++ /dev/null
@@ -1,1966 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class BlameRangeCommit < Grape::Entity
- expose :id
- expose :parent_ids
- expose :message
- expose :authored_date, :author_name, :author_email
- expose :committed_date, :committer_name, :committer_email
- end
-
- class BlameRange < Grape::Entity
- expose :commit, using: BlameRangeCommit
- expose :lines
- end
-
- class WikiPageBasic < Grape::Entity
- expose :format
- expose :slug
- expose :title
- end
-
- class WikiPage < WikiPageBasic
- expose :content
- end
-
- class WikiAttachment < Grape::Entity
- include Gitlab::FileMarkdownLinkBuilder
-
- expose :file_name
- expose :file_path
- expose :branch
- expose :link do
- expose :file_path, as: :url
- expose :markdown do |_entity|
- self.markdown_link
- end
- end
-
- def filename
- object.file_name
- end
-
- def secure_url
- object.file_path
- end
- end
-
- class UserSafe < Grape::Entity
- expose :id, :name, :username
- end
-
- class UserBasic < UserSafe
- expose :state
-
- expose :avatar_url do |user, options|
- user.avatar_url(only_path: false)
- end
-
- expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path }
- expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
-
- expose :web_url do |user, options|
- Gitlab::Routing.url_helpers.user_url(user)
- end
- end
-
- class User < UserBasic
- expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
- expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization
- end
-
- class UserActivity < Grape::Entity
- expose :username
- expose :last_activity_on
- expose :last_activity_on, as: :last_activity_at # Back-compat
- end
-
- class UserStarsProject < Grape::Entity
- expose :starred_since
- expose :user, using: Entities::UserBasic
- end
-
- class Identity < Grape::Entity
- expose :provider, :extern_uid
- end
-
- class UserPublic < User
- expose :last_sign_in_at
- expose :confirmed_at
- expose :last_activity_on
- expose :email
- expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
- expose :identities, using: Entities::Identity
- expose :can_create_group?, as: :can_create_group
- expose :can_create_project?, as: :can_create_project
- expose :two_factor_enabled?, as: :two_factor_enabled
- expose :external
- expose :private_profile
- end
-
- class UserWithAdmin < UserPublic
- expose :admin?, as: :is_admin
- end
-
- class UserDetailsWithAdmin < UserWithAdmin
- expose :highest_role
- end
-
- class UserStatus < Grape::Entity
- expose :emoji
- expose :message
- expose :message_html do |entity|
- MarkupHelper.markdown_field(entity, :message)
- end
- end
-
- class Email < Grape::Entity
- expose :id, :email
- end
-
- class Hook < Grape::Entity
- expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events
- expose :enable_ssl_verification
- end
-
- class ProjectHook < Hook
- expose :project_id, :issues_events, :confidential_issues_events
- expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events
- expose :job_events
- expose :push_events_branch_filter
- end
-
- class SharedGroup < Grape::Entity
- expose :group_id
- expose :group_name do |group_link, options|
- group_link.group.name
- end
- expose :group_full_path do |group_link, options|
- group_link.group.full_path
- end
- expose :group_access, as: :group_access_level
- expose :expires_at
- end
-
- class ProjectIdentity < Grape::Entity
- expose :id, :description
- expose :name, :name_with_namespace
- expose :path, :path_with_namespace
- expose :created_at
- end
-
- class ProjectExportStatus < ProjectIdentity
- include ::API::Helpers::RelatedResourcesHelpers
-
- expose :export_status
- expose :_links, if: lambda { |project, _options| project.export_status == :finished } do
- expose :api_url do |project|
- expose_url(api_v4_projects_export_download_path(id: project.id))
- end
-
- expose :web_url do |project|
- Gitlab::Routing.url_helpers.download_export_project_url(project)
- end
- end
- end
-
- class RemoteMirror < Grape::Entity
- expose :id
- expose :enabled
- expose :safe_url, as: :url
- expose :update_status
- expose :last_update_at
- expose :last_update_started_at
- expose :last_successful_update_at
- expose :last_error
- expose :only_protected_branches
- end
-
- class ContainerExpirationPolicy < Grape::Entity
- expose :cadence
- expose :enabled
- expose :keep_n
- expose :older_than
- expose :name_regex
- expose :next_run_at
- end
-
- class ProjectImportStatus < ProjectIdentity
- expose :import_status
-
- # TODO: Use `expose_nil` once we upgrade the grape-entity gem
- expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project|
- project.import_state.last_error
- end
- end
-
- class BasicProjectDetails < ProjectIdentity
- include ::API::ProjectsRelationBuilder
-
- expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
- # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
- expose :tag_list do |project|
- # project.tags.order(:name).pluck(:name) is the most suitable option
- # to avoid loading all the ActiveRecord objects but, if we use it here
- # it override the preloaded associations and makes a query
- # (fixed in https://github.com/rails/rails/pull/25976).
- project.tags.map(&:name).sort
- end
-
- expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
-
- expose :license_url, if: :license do |project|
- license = project.repository.license_blob
-
- if license
- Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path))
- end
- end
-
- expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project|
- project.repository.license
- end
-
- expose :avatar_url do |project, options|
- project.avatar_url(only_path: false)
- end
-
- expose :star_count, :forks_count
- expose :last_activity_at
- expose :namespace, using: 'API::Entities::NamespaceBasic'
- expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
-
- # rubocop: disable CodeReuse/ActiveRecord
- def self.preload_relation(projects_relation, options = {})
- # Preloading tags, should be done with using only `:tags`,
- # as `:tags` are defined as: `has_many :tags, through: :taggings`
- # N+1 is solved then by using `subject.tags.map(&:name)`
- # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555
- projects_relation.preload(:project_feature, :route)
- .preload(:import_state, :tags)
- .preload(:auto_devops)
- .preload(namespace: [:route, :owner])
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- class Project < BasicProjectDetails
- include ::API::Helpers::RelatedResourcesHelpers
-
- expose :_links do
- expose :self do |project|
- expose_url(api_v4_projects_path(id: project.id))
- end
-
- expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project|
- expose_url(api_v4_projects_issues_path(id: project.id))
- end
-
- expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project|
- expose_url(api_v4_projects_merge_requests_path(id: project.id))
- end
-
- expose :repo_branches do |project|
- expose_url(api_v4_projects_repository_branches_path(id: project.id))
- end
-
- expose :labels do |project|
- expose_url(api_v4_projects_labels_path(id: project.id))
- end
-
- expose :events do |project|
- expose_url(api_v4_projects_events_path(id: project.id))
- end
-
- expose :members do |project|
- expose_url(api_v4_projects_members_path(id: project.id))
- end
- end
-
- expose :empty_repo?, as: :empty_repo
- expose :archived?, as: :archived
- expose :visibility
- expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
- expose :resolve_outdated_diff_discussions
- expose :container_registry_enabled
- expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy,
- if: -> (project, _) { project.container_expiration_policy }
-
- # Expose old field names with the new permissions methods to keep API compatible
- # TODO: remove in API v5, replaced by *_access_level
- expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
- expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
- expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
- expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
- expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
-
- expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) }
- expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) }
- expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) }
- expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) }
- expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
- expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
-
- expose :shared_runners_enabled
- expose :lfs_enabled?, as: :lfs_enabled
- expose :creator_id
- expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do
- project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project)
- end
- expose :import_status
-
- expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project|
- project.import_state&.last_error
- end
-
- expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
- expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :ci_default_git_depth
- expose :public_builds, as: :public_jobs
- expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options|
- project.build_allow_git_fetch ? 'fetch' : 'clone'
- end
- expose :build_timeout
- expose :auto_cancel_pending_pipelines
- expose :build_coverage_regex
- expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
- expose :shared_with_groups do |project, options|
- SharedGroup.represent(project.project_group_links, options)
- end
- expose :only_allow_merge_if_pipeline_succeeds
- expose :request_access_enabled
- expose :only_allow_merge_if_all_discussions_are_resolved
- expose :remove_source_branch_after_merge
- expose :printing_merge_request_link_enabled
- expose :merge_method
- expose :suggestion_commit_message
- expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
- options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
- }
- expose :auto_devops_enabled?, as: :auto_devops_enabled
- expose :auto_devops_deploy_strategy do |project, options|
- project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
- end
- expose :autoclose_referenced_issues
-
- # rubocop: disable CodeReuse/ActiveRecord
- def self.preload_relation(projects_relation, options = {})
- # Preloading tags, should be done with using only `:tags`,
- # as `:tags` are defined as: `has_many :tags, through: :taggings`
- # N+1 is solved then by using `subject.tags.map(&:name)`
- # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555
- super(projects_relation).preload(:group)
- .preload(:ci_cd_settings)
- .preload(:container_expiration_policy)
- .preload(:auto_devops)
- .preload(project_group_links: { group: :route },
- fork_network: :root_project,
- fork_network_member: :forked_from_project,
- forked_from_project: [:route, :forks, :tags, namespace: :route])
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def self.forks_counting_projects(projects_relation)
- projects_relation + projects_relation.map(&:forked_from_project).compact
- end
- end
-
- class ProjectStatistics < Grape::Entity
- expose :commit_count
- expose :storage_size
- expose :repository_size
- expose :wiki_size
- expose :lfs_objects_size
- expose :build_artifacts_size, as: :job_artifacts_size
- end
-
- class ProjectDailyFetches < Grape::Entity
- expose :fetch_count, as: :count
- expose :date
- end
-
- class ProjectDailyStatistics < Grape::Entity
- expose :fetches do
- expose :total_fetch_count, as: :total
- expose :fetches, as: :days, using: ProjectDailyFetches
- end
- end
-
- class Member < Grape::Entity
- expose :user, merge: true, using: UserBasic
- expose :access_level
- expose :expires_at
- end
-
- class AccessRequester < Grape::Entity
- expose :user, merge: true, using: UserBasic
- expose :requested_at
- end
-
- class BasicGroupDetails < Grape::Entity
- expose :id
- expose :web_url
- expose :name
- end
-
- class Group < BasicGroupDetails
- expose :path, :description, :visibility
- expose :share_with_group_lock
- expose :require_two_factor_authentication
- expose :two_factor_grace_period
- expose :project_creation_level_str, as: :project_creation_level
- expose :auto_devops_enabled
- expose :subgroup_creation_level_str, as: :subgroup_creation_level
- expose :emails_disabled
- expose :mentions_disabled
- expose :lfs_enabled?, as: :lfs_enabled
- expose :avatar_url do |group, options|
- group.avatar_url(only_path: false)
- end
- expose :request_access_enabled
- expose :full_name, :full_path
- expose :parent_id
-
- expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
-
- expose :statistics, if: :statistics do
- with_options format_with: -> (value) { value.to_i } do
- expose :storage_size
- expose :repository_size
- expose :wiki_size
- expose :lfs_objects_size
- expose :build_artifacts_size, as: :job_artifacts_size
- end
- end
- end
-
- class GroupDetail < Group
- expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] }
- expose :projects, using: Entities::Project do |group, options|
- projects = GroupProjectsFinder.new(
- group: group,
- current_user: options[:current_user],
- options: { only_owned: true, limit: projects_limit }
- ).execute
-
- Entities::Project.prepare_relation(projects)
- end
-
- expose :shared_projects, using: Entities::Project do |group, options|
- projects = GroupProjectsFinder.new(
- group: group,
- current_user: options[:current_user],
- options: { only_shared: true, limit: projects_limit }
- ).execute
-
- Entities::Project.prepare_relation(projects)
- end
-
- def projects_limit
- if ::Feature.enabled?(:limit_projects_in_groups_api, default_enabled: true)
- GroupProjectsFinder::DEFAULT_PROJECTS_LIMIT
- else
- nil
- end
- end
- end
-
- class DiffRefs < Grape::Entity
- expose :base_sha, :head_sha, :start_sha
- end
-
- class Commit < Grape::Entity
- expose :id, :short_id, :created_at
- expose :parent_ids
- expose :full_title, as: :title
- expose :safe_message, as: :message
- expose :author_name, :author_email, :authored_date
- expose :committer_name, :committer_email, :committed_date
- end
-
- class CommitStats < Grape::Entity
- expose :additions, :deletions, :total
- end
-
- class CommitWithStats < Commit
- expose :stats, using: Entities::CommitStats
- end
-
- class CommitDetail < Commit
- expose :stats, using: Entities::CommitStats, if: :stats
- expose :status
- expose :project_id
-
- expose :last_pipeline do |commit, options|
- pipeline = commit.last_pipeline if can_read_pipeline?
- ::API::Entities::PipelineBasic.represent(pipeline, options)
- end
-
- private
-
- def can_read_pipeline?
- Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
- end
- end
-
- class CommitSignature < Grape::Entity
- expose :gpg_key_id
- expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email
- expose :verification_status
- expose :gpg_key_subkey_id
- end
-
- class BasicRef < Grape::Entity
- expose :type, :name
- end
-
- class Branch < Grape::Entity
- expose :name
-
- expose :commit, using: Entities::Commit do |repo_branch, options|
- options[:project].repository.commit(repo_branch.dereferenced_target)
- end
-
- expose :merged do |repo_branch, options|
- if options[:merged_branch_names]
- options[:merged_branch_names].include?(repo_branch.name)
- else
- options[:project].repository.merged_to_root_ref?(repo_branch)
- end
- end
-
- expose :protected do |repo_branch, options|
- ::ProtectedBranch.protected?(options[:project], repo_branch.name)
- end
-
- expose :developers_can_push do |repo_branch, options|
- ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
- end
-
- expose :developers_can_merge do |repo_branch, options|
- ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
- end
-
- expose :can_push do |repo_branch, options|
- Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name)
- end
-
- expose :default do |repo_branch, options|
- options[:project].default_branch == repo_branch.name
- end
- end
-
- class TreeObject < Grape::Entity
- expose :id, :name, :type, :path
-
- expose :mode do |obj, options|
- filemode = obj.mode
- filemode = "0" + filemode if filemode.length < 6
- filemode
- end
- end
-
- class Snippet < Grape::Entity
- expose :id, :title, :file_name, :description, :visibility
- expose :author, using: Entities::UserBasic
- expose :updated_at, :created_at
- expose :project_id
- expose :web_url do |snippet|
- Gitlab::UrlBuilder.build(snippet)
- end
- end
-
- class ProjectSnippet < Snippet
- end
-
- class PersonalSnippet < Snippet
- expose :raw_url do |snippet|
- Gitlab::UrlBuilder.build(snippet, raw: true)
- end
- end
-
- class IssuableEntity < Grape::Entity
- expose :id, :iid
- expose(:project_id) { |entity| entity&.project.try(:id) }
- expose :title, :description
- expose :state, :created_at, :updated_at
-
- # Avoids an N+1 query when metadata is included
- def issuable_metadata(subject, options, method, args = nil)
- cached_subject = options.dig(:issuable_metadata, subject.id)
- (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
- end
- end
-
- class IssuableReferences < Grape::Entity
- expose :short do |issuable|
- issuable.to_reference
- end
-
- expose :relative do |issuable, options|
- issuable.to_reference(options[:group] || options[:project])
- end
-
- expose :full do |issuable|
- issuable.to_reference(full: true)
- end
- end
-
- class Diff < Grape::Entity
- expose :old_path, :new_path, :a_mode, :b_mode
- expose :new_file?, as: :new_file
- expose :renamed_file?, as: :renamed_file
- expose :deleted_file?, as: :deleted_file
- expose :json_safe_diff, as: :diff
- end
-
- class ProtectedRefAccess < Grape::Entity
- expose :access_level
- expose :access_level_description do |protected_ref_access|
- protected_ref_access.humanize
- end
- end
-
- class ProtectedBranch < Grape::Entity
- expose :id
- expose :name
- expose :push_access_levels, using: Entities::ProtectedRefAccess
- expose :merge_access_levels, using: Entities::ProtectedRefAccess
- end
-
- class ProtectedTag < Grape::Entity
- expose :name
- expose :create_access_levels, using: Entities::ProtectedRefAccess
- end
-
- class Milestone < Grape::Entity
- expose :id, :iid
- expose :project_id, if: -> (entity, options) { entity&.project_id }
- expose :group_id, if: -> (entity, options) { entity&.group_id }
- expose :title, :description
- expose :state, :created_at, :updated_at
- expose :due_date
- expose :start_date
-
- expose :web_url do |milestone, _options|
- Gitlab::UrlBuilder.build(milestone)
- end
- end
-
- class IssueBasic < IssuableEntity
- expose :closed_at
- expose :closed_by, using: Entities::UserBasic
-
- expose :labels do |issue, options|
- if options[:with_labels_details]
- ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
- else
- issue.labels.map(&:title).sort
- end
- end
-
- expose :milestone, using: Entities::Milestone
- expose :assignees, :author, using: Entities::UserBasic
-
- expose :assignee, using: ::API::Entities::UserBasic do |issue|
- issue.assignees.first
- end
-
- expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
- expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
- expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
- expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
- expose :due_date
- expose :confidential
- expose :discussion_locked
-
- expose :web_url do |issue|
- Gitlab::UrlBuilder.build(issue)
- end
-
- expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
- issue
- end
-
- expose :task_completion_status
- end
-
- class Issue < IssueBasic
- include ::API::Helpers::RelatedResourcesHelpers
-
- expose(:has_tasks) do |issue, _|
- !issue.task_list_items.empty?
- end
-
- expose :task_status, if: -> (issue, _) do
- !issue.task_list_items.empty?
- end
-
- expose :_links do
- expose :self do |issue|
- expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
- end
-
- expose :notes do |issue|
- expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
- end
-
- expose :award_emoji do |issue|
- expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
- end
-
- expose :project do |issue|
- expose_url(api_v4_projects_path(id: issue.project_id))
- end
- end
-
- expose :references, with: IssuableReferences do |issue|
- issue
- end
-
- # Calculating the value of subscribed field triggers Markdown
- # processing. We can't do that for multiple issues / merge
- # requests in a single API request.
- expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
- issue.subscribed?(options[:current_user], options[:project] || issue.project)
- end
-
- expose :moved_to_id
- end
-
- class IssuableTimeStats < Grape::Entity
- format_with(:time_tracking_formatter) do |time_spent|
- Gitlab::TimeTrackingFormatter.output(time_spent)
- end
-
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
-
- with_options(format_with: :time_tracking_formatter) do
- expose :total_time_spent, as: :human_total_time_spent
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def total_time_spent
- # Avoids an N+1 query since timelogs are preloaded
- object.timelogs.map(&:time_spent).sum
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- class ExternalIssue < Grape::Entity
- expose :title
- expose :id
- end
-
- class PipelineBasic < Grape::Entity
- expose :id, :sha, :ref, :status
- expose :created_at, :updated_at
-
- expose :web_url do |pipeline, _options|
- Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
- end
- end
-
- class MergeRequestSimple < IssuableEntity
- expose :title
- expose :web_url do |merge_request, options|
- Gitlab::UrlBuilder.build(merge_request)
- end
- end
-
- class MergeRequestBasic < IssuableEntity
- expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.merged_by
- end
-
- expose :merged_at do |merge_request, _options|
- merge_request.metrics&.merged_at
- end
-
- expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.latest_closed_by
- end
-
- expose :closed_at do |merge_request, _options|
- merge_request.metrics&.latest_closed_at
- end
-
- expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
- MarkupHelper.markdown_field(entity, :title)
- end
- expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
- MarkupHelper.markdown_field(entity, :description)
- end
- expose :target_branch, :source_branch
- expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
- expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
- expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
- expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
- merge_request.assignee
- end
- expose :author, :assignees, using: Entities::UserBasic
-
- expose :source_project_id, :target_project_id
- expose :labels do |merge_request, options|
- if options[:with_labels_details]
- ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title))
- else
- merge_request.labels.map(&:title).sort
- end
- end
- expose :work_in_progress?, as: :work_in_progress
- expose :milestone, using: Entities::Milestone
- expose :merge_when_pipeline_succeeds
-
- # Ideally we should deprecate `MergeRequest#merge_status` exposure and
- # use `MergeRequest#mergeable?` instead (boolean).
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
- # information.
- expose :merge_status do |merge_request|
- merge_request.check_mergeability
- merge_request.merge_status
- end
- expose :diff_head_sha, as: :sha
- expose :merge_commit_sha
- expose :squash_commit_sha
- expose :discussion_locked
- expose :should_remove_source_branch?, as: :should_remove_source_branch
- expose :force_remove_source_branch?, as: :force_remove_source_branch
- expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
- # Deprecated
- expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
-
- # reference is deprecated in favour of references
- # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354)
- expose :reference do |merge_request, options|
- merge_request.to_reference(options[:project])
- end
-
- expose :references, with: IssuableReferences do |merge_request|
- merge_request
- end
-
- expose :web_url do |merge_request|
- Gitlab::UrlBuilder.build(merge_request)
- end
-
- expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
- merge_request
- end
-
- expose :squash
-
- expose :task_completion_status
-
- expose :cannot_be_merged?, as: :has_conflicts
-
- expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
- end
-
- class MergeRequest < MergeRequestBasic
- expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options|
- merge_request.subscribed?(options[:current_user], options[:project])
- end
-
- expose :changes_count do |merge_request, _options|
- merge_request.merge_request_diff.real_size
- end
-
- expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.latest_build_started_at
- end
-
- expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.latest_build_finished_at
- end
-
- expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.first_deployed_to_production_at
- end
-
- expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
- merge_request.metrics&.pipeline
- end
-
- expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
- Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
- end
-
- expose :diff_refs, using: Entities::DiffRefs
-
- # Allow the status of a rebase to be determined
- expose :merge_error
- expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] }
-
- expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] }
-
- def build_available?(options)
- options[:project]&.feature_available?(:builds, options[:current_user])
- end
-
- expose :user do
- expose :can_merge do |merge_request, options|
- merge_request.can_be_merged_by?(options[:current_user])
- end
- end
- end
-
- class MergeRequestChanges < MergeRequest
- expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
- compare.raw_diffs(limits: false).to_a
- end
- end
-
- class MergeRequestDiff < Grape::Entity
- expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
- :created_at, :merge_request_id, :state, :real_size
- end
-
- class MergeRequestDiffFull < MergeRequestDiff
- expose :commits, using: Entities::Commit
-
- expose :diffs, using: Entities::Diff do |compare, _|
- compare.raw_diffs(limits: false).to_a
- end
- end
-
- class SSHKey < Grape::Entity
- expose :id, :title, :key, :created_at
- end
-
- class SSHKeyWithUser < SSHKey
- expose :user, using: Entities::UserPublic
- end
-
- class DeployKeyWithUser < SSHKeyWithUser
- expose :deploy_keys_projects
- end
-
- class DeployKeysProject < Grape::Entity
- expose :deploy_key, merge: true, using: Entities::SSHKey
- expose :can_push
- end
-
- class GPGKey < Grape::Entity
- expose :id, :key, :created_at
- end
-
- class DiffPosition < Grape::Entity
- expose :base_sha, :start_sha, :head_sha, :old_path, :new_path,
- :position_type
- end
-
- class Note < Grape::Entity
- # Only Issue and MergeRequest have iid
- NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
-
- expose :id
- expose :type
- expose :note, as: :body
- expose :attachment_identifier, as: :attachment
- expose :author, using: Entities::UserBasic
- expose :created_at, :updated_at
- expose :system?, as: :system
- expose :noteable_id, :noteable_type
-
- expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note|
- note.position.to_h
- end
-
- expose :resolvable?, as: :resolvable
- expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? }
- expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? }
-
- # Avoid N+1 queries as much as possible
- expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
- end
-
- class Discussion < Grape::Entity
- expose :id
- expose :individual_note?, as: :individual_note
- expose :notes, using: Entities::Note
- end
-
- class Avatar < Grape::Entity
- expose :avatar_url do |avatarable, options|
- avatarable.avatar_url(only_path: false, size: options[:size])
- end
- end
-
- class AwardEmoji < Grape::Entity
- expose :id
- expose :name
- expose :user, using: Entities::UserBasic
- expose :created_at, :updated_at
- expose :awardable_id, :awardable_type
- end
-
- class MRNote < Grape::Entity
- expose :note
- expose :author, using: Entities::UserBasic
- end
-
- class CommitNote < Grape::Entity
- expose :note
- expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? }
- expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? }
- expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? }
- expose :author, using: Entities::UserBasic
- expose :created_at
- end
-
- class CommitStatus < Grape::Entity
- expose :id, :sha, :ref, :status, :name, :target_url, :description,
- :created_at, :started_at, :finished_at, :allow_failure, :coverage
- expose :author, using: Entities::UserBasic
- end
-
- class PushEventPayload < Grape::Entity
- expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref,
- :commit_title, :ref_count
- end
-
- class Event < Grape::Entity
- expose :project_id, :action_name
- expose :target_id, :target_iid, :target_type, :author_id
- expose :target_title
- expose :created_at
- expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
- expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
-
- expose :push_event_payload,
- as: :push_data,
- using: PushEventPayload,
- if: -> (event, _) { event.push_action? }
-
- expose :author_username do |event, options|
- event.author&.username
- end
- end
-
- class ProjectGroupLink < Grape::Entity
- expose :id, :project_id, :group_id, :group_access, :expires_at
- end
-
- class Todo < Grape::Entity
- expose :id
- expose :project, using: Entities::ProjectIdentity, if: -> (todo, _) { todo.project_id }
- expose :group, using: 'API::Entities::NamespaceBasic', if: -> (todo, _) { todo.group_id }
- expose :author, using: Entities::UserBasic
- expose :action_name
- expose :target_type
-
- expose :target do |todo, options|
- todo_options = options.fetch(todo.target_type, {})
- todo_target_class(todo.target_type).represent(todo.target, todo_options)
- end
-
- expose :target_url do |todo, options|
- todo_target_url(todo)
- end
-
- expose :body
- expose :state
- expose :created_at
-
- def todo_target_class(target_type)
- # false as second argument prevents looking up in module hierarchy
- # see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719
- ::API::Entities.const_get(target_type, false)
- end
-
- def todo_target_url(todo)
- target_type = todo.target_type.underscore
- target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
-
- Gitlab::Routing
- .url_helpers
- .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def todo_target_anchor(todo)
- "note_#{todo.note_id}" if todo.note_id?
- end
- end
-
- class NamespaceBasic < Grape::Entity
- expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url
-
- expose :web_url do |namespace|
- if namespace.user?
- Gitlab::Routing.url_helpers.user_url(namespace.owner)
- else
- namespace.web_url
- end
- end
- end
-
- class Namespace < NamespaceBasic
- expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _|
- namespace.users_with_descendants.count
- end
-
- def expose_members_count_with_descendants?(namespace, opts)
- namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
- end
- end
-
- class MemberAccess < Grape::Entity
- expose :access_level
- expose :notification_level do |member, options|
- if member.notification_setting
- ::NotificationSetting.levels[member.notification_setting.level]
- end
- end
- end
-
- class ProjectAccess < MemberAccess
- end
-
- class GroupAccess < MemberAccess
- end
-
- class NotificationSetting < Grape::Entity
- expose :level
- expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
- ::NotificationSetting.email_events.each do |event|
- expose event
- end
- end
- end
-
- class GlobalNotificationSetting < NotificationSetting
- expose :notification_email do |notification_setting, options|
- notification_setting.user.notification_email
- end
- end
-
- class ProjectServiceBasic < Grape::Entity
- expose :id, :title
- expose :slug do |service|
- service.to_param.dasherize
- end
- expose :created_at, :updated_at, :active
- expose :commit_events, :push_events, :issues_events, :confidential_issues_events
- expose :merge_requests_events, :tag_push_events, :note_events
- expose :confidential_note_events, :pipeline_events, :wiki_page_events
- expose :job_events, :comment_on_event_enabled
- end
-
- class ProjectService < ProjectServiceBasic
- # Expose serialized properties
- expose :properties do |service, options|
- # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
- if service.data_fields_present?
- service.data_fields.as_json.slice(*service.api_field_names)
- else
- service.properties.slice(*service.api_field_names)
- end
- end
- end
-
- class ProjectWithAccess < Project
- expose :permissions do
- expose :project_access, using: Entities::ProjectAccess do |project, options|
- if options[:project_members]
- options[:project_members].find { |member| member.source_id == project.id }
- else
- project.project_member(options[:current_user])
- end
- end
-
- expose :group_access, using: Entities::GroupAccess do |project, options|
- if project.group
- if options[:group_members]
- options[:group_members].find { |member| member.source_id == project.namespace_id }
- else
- project.group.highest_group_member(options[:current_user])
- end
- end
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def self.preload_relation(projects_relation, options = {})
- relation = super(projects_relation, options)
- project_ids = relation.select('projects.id')
- namespace_ids = relation.select(:namespace_id)
-
- options[:project_members] = options[:current_user]
- .project_members
- .where(source_id: project_ids)
- .preload(:source, user: [notification_settings: :source])
-
- options[:group_members] = options[:current_user]
- .group_members
- .where(source_id: namespace_ids)
- .preload(:source, user: [notification_settings: :source])
-
- relation
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- class LabelBasic < Grape::Entity
- expose :id, :name, :color, :description, :description_html, :text_color
- end
-
- class Label < LabelBasic
- with_options if: lambda { |_, options| options[:with_counts] } do
- expose :open_issues_count do |label, options|
- label.open_issues_count(options[:current_user])
- end
-
- expose :closed_issues_count do |label, options|
- label.closed_issues_count(options[:current_user])
- end
-
- expose :open_merge_requests_count do |label, options|
- label.open_merge_requests_count(options[:current_user])
- end
- end
-
- expose :subscribed do |label, options|
- label.subscribed?(options[:current_user], options[:parent])
- end
- end
-
- class GroupLabel < Label
- end
-
- class ProjectLabel < Label
- expose :priority do |label, options|
- label.priority(options[:parent])
- end
- expose :is_project_label do |label, options|
- label.is_a?(::ProjectLabel)
- end
- end
-
- class List < Grape::Entity
- expose :id
- expose :label, using: Entities::LabelBasic
- expose :position
- end
-
- class Board < Grape::Entity
- expose :id
- expose :project, using: Entities::BasicProjectDetails
-
- expose :lists, using: Entities::List do |board|
- board.destroyable_lists
- end
- end
-
- class Compare < Grape::Entity
- expose :commit, using: Entities::Commit do |compare, options|
- ::Commit.decorate(compare.commits, nil).last
- end
-
- expose :commits, using: Entities::Commit do |compare, options|
- ::Commit.decorate(compare.commits, nil)
- end
-
- expose :diffs, using: Entities::Diff do |compare, options|
- compare.diffs(limits: false).to_a
- end
-
- expose :compare_timeout do |compare, options|
- compare.diffs.overflow?
- end
-
- expose :same, as: :compare_same_ref
- end
-
- class Contributor < Grape::Entity
- expose :name, :email, :commits, :additions, :deletions
- end
-
- class BroadcastMessage < Grape::Entity
- expose :message, :starts_at, :ends_at, :color, :font, :target_path
- end
-
- class ApplicationStatistics < Grape::Entity
- include ActionView::Helpers::NumberHelper
- include CountHelper
-
- expose :forks do |counts|
- approximate_fork_count_with_delimiters(counts)
- end
-
- expose :issues do |counts|
- approximate_count_with_delimiters(counts, ::Issue)
- end
-
- expose :merge_requests do |counts|
- approximate_count_with_delimiters(counts, ::MergeRequest)
- end
-
- expose :notes do |counts|
- approximate_count_with_delimiters(counts, ::Note)
- end
-
- expose :snippets do |counts|
- approximate_count_with_delimiters(counts, ::Snippet)
- end
-
- expose :ssh_keys do |counts|
- approximate_count_with_delimiters(counts, ::Key)
- end
-
- expose :milestones do |counts|
- approximate_count_with_delimiters(counts, ::Milestone)
- end
-
- expose :users do |counts|
- approximate_count_with_delimiters(counts, ::User)
- end
-
- expose :projects do |counts|
- approximate_count_with_delimiters(counts, ::Project)
- end
-
- expose :groups do |counts|
- approximate_count_with_delimiters(counts, ::Group)
- end
-
- expose :active_users do |_|
- number_with_delimiter(::User.active.count)
- end
- end
-
- class ApplicationSetting < Grape::Entity
- def self.exposed_attributes
- attributes = ::ApplicationSettingsHelper.visible_attributes
- attributes.delete(:performance_bar_allowed_group_path)
- attributes.delete(:performance_bar_enabled)
- attributes.delete(:allow_local_requests_from_hooks_and_services)
-
- # let's not expose the secret key in a response
- attributes.delete(:asset_proxy_secret_key)
- attributes.delete(:eks_secret_access_key)
-
- attributes
- end
-
- expose :id, :performance_bar_allowed_group_id
- expose(*exposed_attributes)
- expose(:restricted_visibility_levels) do |setting, _options|
- setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
- end
- expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
- expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
- expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
-
- expose(*::ApplicationSettingsHelper.external_authorization_service_attributes)
-
- # support legacy names, can be removed in v5
- expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
- expose :password_authentication_enabled_for_web, as: :signin_enabled
- expose :allow_local_requests_from_web_hooks_and_services, as: :allow_local_requests_from_hooks_and_services
- end
-
- class Appearance < Grape::Entity
- expose :title
- expose :description
-
- expose :logo do |appearance, options|
- appearance.logo.url
- end
-
- expose :header_logo do |appearance, options|
- appearance.header_logo.url
- end
-
- expose :favicon do |appearance, options|
- appearance.favicon.url
- end
-
- expose :new_project_guidelines
- expose :header_message
- expose :footer_message
- expose :message_background_color
- expose :message_font_color
- expose :email_header_and_footer_enabled
- end
-
- # deprecated old Release representation
- class TagRelease < Grape::Entity
- expose :tag, as: :tag_name
- expose :description
- end
-
- module Releases
- class Link < Grape::Entity
- expose :id
- expose :name
- expose :url
- expose :external?, as: :external
- end
-
- class Source < Grape::Entity
- expose :format
- expose :url
- end
- end
-
- class Release < Grape::Entity
- include ::API::Helpers::Presentable
-
- expose :name do |release, _|
- can_download_code? ? release.name : "Release-#{release.id}"
- end
- expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
- expose :description
- expose :description_html do |entity|
- MarkupHelper.markdown_field(entity, :description)
- end
- expose :created_at
- expose :released_at
- expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
- expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
- expose :upcoming_release?, as: :upcoming_release
- expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? && can_read_milestone? }
- expose :commit_path, expose_nil: false
- expose :tag_path, expose_nil: false
- expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
- expose :assets do
- expose :assets_count, as: :count do |release, _|
- assets_to_exclude = can_download_code? ? [] : [:sources]
- release.assets_count(except: assets_to_exclude)
- end
- expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
- expose :links, using: Entities::Releases::Link do |release, options|
- release.links.sorted
- end
- expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
- end
- expose :_links do
- expose :merge_requests_url, expose_nil: false
- expose :issues_url, expose_nil: false
- expose :edit_url, expose_nil: false
- end
-
- private
-
- def can_download_code?
- Ability.allowed?(options[:current_user], :download_code, object.project)
- end
-
- def can_read_milestone?
- Ability.allowed?(options[:current_user], :read_milestone, object.project)
- end
- end
-
- class Tag < Grape::Entity
- expose :name, :message, :target
-
- expose :commit, using: Entities::Commit do |repo_tag, options|
- options[:project].repository.commit(repo_tag.dereferenced_target)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- expose :release, using: Entities::TagRelease do |repo_tag, options|
- options[:project].releases.find_by(tag: repo_tag.name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- expose :protected do |repo_tag, options|
- ::ProtectedTag.protected?(options[:project], repo_tag.name)
- end
- end
-
- class Runner < Grape::Entity
- expose :id
- expose :description
- expose :ip_address
- expose :active
- expose :instance_type?, as: :is_shared
- expose :name
- expose :online?, as: :online
- expose :status
- end
-
- class RunnerDetails < Runner
- expose :tag_list
- expose :run_untagged
- expose :locked
- expose :maximum_timeout
- expose :access_level
- expose :version, :revision, :platform, :architecture
- expose :contacted_at
- expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
- # rubocop: disable CodeReuse/ActiveRecord
- expose :projects, with: Entities::BasicProjectDetails do |runner, options|
- if options[:current_user].admin?
- runner.projects
- else
- options[:current_user].authorized_projects.where(id: runner.projects)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- expose :groups, with: Entities::BasicGroupDetails do |runner, options|
- if options[:current_user].admin?
- runner.groups
- else
- options[:current_user].authorized_groups.where(id: runner.groups)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- class RunnerRegistrationDetails < Grape::Entity
- expose :id, :token
- end
-
- class JobArtifactFile < Grape::Entity
- expose :filename
- expose :cached_size, as: :size
- end
-
- class JobArtifact < Grape::Entity
- expose :file_type, :size, :filename, :file_format
- end
-
- class JobBasic < Grape::Entity
- expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
- expose :created_at, :started_at, :finished_at
- expose :duration
- expose :user, with: User
- expose :commit, with: Commit
- expose :pipeline, with: PipelineBasic
-
- expose :web_url do |job, _options|
- Gitlab::Routing.url_helpers.project_job_url(job.project, job)
- end
- end
-
- class Job < JobBasic
- # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
- expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
- expose :job_artifacts, as: :artifacts, using: JobArtifact
- expose :runner, with: Runner
- expose :artifacts_expire_at
- end
-
- class JobBasicWithProject < JobBasic
- expose :project, with: ProjectIdentity
- end
-
- class Trigger < Grape::Entity
- include ::API::Helpers::Presentable
-
- expose :id
- expose :token
- expose :description
- expose :created_at, :updated_at, :last_used
- expose :owner, using: Entities::UserBasic
- end
-
- class Variable < Grape::Entity
- expose :variable_type, :key, :value
- expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
- expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
- expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
- end
-
- class Pipeline < PipelineBasic
- expose :before_sha, :tag, :yaml_errors
-
- expose :user, with: Entities::UserBasic
- expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
- expose :duration
- expose :coverage
- expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
- pipeline.detailed_status(options[:current_user])
- end
- end
-
- class PipelineSchedule < Grape::Entity
- expose :id
- expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
- expose :created_at, :updated_at
- expose :owner, using: Entities::UserBasic
- end
-
- class PipelineScheduleDetails < PipelineSchedule
- expose :last_pipeline, using: Entities::PipelineBasic
- expose :variables, using: Entities::Variable
- end
-
- class EnvironmentBasic < Grape::Entity
- expose :id, :name, :slug, :external_url
- end
-
- class Deployment < Grape::Entity
- expose :id, :iid, :ref, :sha, :created_at, :updated_at
- expose :user, using: Entities::UserBasic
- expose :environment, using: Entities::EnvironmentBasic
- expose :deployable, using: Entities::Job
- expose :status
- end
-
- class Environment < EnvironmentBasic
- expose :project, using: Entities::BasicProjectDetails
- expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
- expose :state
- end
-
- class LicenseBasic < Grape::Entity
- expose :key, :name, :nickname
- expose :url, as: :html_url
- expose(:source_url) { |license| license.meta['source'] }
- end
-
- class License < LicenseBasic
- expose :popular?, as: :popular
- expose(:description) { |license| license.meta['description'] }
- expose(:conditions) { |license| license.meta['conditions'] }
- expose(:permissions) { |license| license.meta['permissions'] }
- expose(:limitations) { |license| license.meta['limitations'] }
- expose :content
- end
-
- class TemplatesList < Grape::Entity
- expose :key
- expose :name
- end
-
- class Template < Grape::Entity
- expose :name, :content
- end
-
- class BroadcastMessage < Grape::Entity
- expose :id, :message, :starts_at, :ends_at, :color, :font
- expose :active?, as: :active
- end
-
- class PersonalAccessToken < Grape::Entity
- expose :id, :name, :revoked, :created_at, :scopes
- expose :active?, as: :active
- expose :expires_at do |personal_access_token|
- personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
- end
- end
-
- class PersonalAccessTokenWithToken < PersonalAccessToken
- expose :token
- end
-
- class ImpersonationToken < PersonalAccessToken
- expose :impersonation
- end
-
- class ImpersonationTokenWithToken < PersonalAccessTokenWithToken
- expose :impersonation
- end
-
- class FeatureGate < Grape::Entity
- expose :key
- expose :value
- end
-
- class Feature < Grape::Entity
- expose :name
- expose :state
- expose :gates, using: FeatureGate do |model|
- model.gates.map do |gate|
- value = model.gate_values[gate.key]
-
- # By default all gate values are populated. Only show relevant ones.
- if (value.is_a?(Integer) && value.zero?) || (value.is_a?(Set) && value.empty?)
- next
- end
-
- { key: gate.key, value: value }
- end.compact
- end
- end
-
- module JobRequest
- class JobInfo < Grape::Entity
- expose :name, :stage
- expose :project_id, :project_name
- end
-
- class GitInfo < Grape::Entity
- expose :repo_url, :ref, :sha, :before_sha
- expose :ref_type
- expose :refspecs
- expose :git_depth, as: :depth
- end
-
- class RunnerInfo < Grape::Entity
- expose :metadata_timeout, as: :timeout
- expose :runner_session_url
- end
-
- class Step < Grape::Entity
- expose :name, :script, :timeout, :when, :allow_failure
- end
-
- class Port < Grape::Entity
- expose :number, :protocol, :name
- end
-
- class Image < Grape::Entity
- expose :name, :entrypoint
- expose :ports, using: JobRequest::Port
- end
-
- class Service < Image
- expose :alias, :command
- end
-
- class Artifacts < Grape::Entity
- expose :name
- expose :untracked
- expose :paths
- expose :when
- expose :expire_in
- expose :artifact_type
- expose :artifact_format
- end
-
- class Cache < Grape::Entity
- expose :key, :untracked, :paths, :policy
- end
-
- class Credentials < Grape::Entity
- expose :type, :url, :username, :password
- end
-
- class Dependency < Grape::Entity
- expose :id, :name, :token
- expose :artifacts_file, using: JobArtifactFile, if: ->(job, _) { job.artifacts? }
- end
-
- class Response < Grape::Entity
- expose :id
- expose :token
- expose :allow_git_fetch
-
- expose :job_info, using: JobInfo do |model|
- model
- end
-
- expose :git_info, using: GitInfo do |model|
- model
- end
-
- expose :runner_info, using: RunnerInfo do |model|
- model
- end
-
- expose :variables
- expose :steps, using: Step
- expose :image, using: Image
- expose :services, using: Service
- expose :artifacts, using: Artifacts
- expose :cache, using: Cache
- expose :credentials, using: Credentials
- expose :all_dependencies, as: :dependencies, using: Dependency
- expose :features
- end
- end
-
- class UserAgentDetail < Grape::Entity
- expose :user_agent
- expose :ip_address
- expose :submitted, as: :akismet_submitted
- end
-
- class CustomAttribute < Grape::Entity
- expose :key
- expose :value
- end
-
- class PagesDomainCertificateExpiration < Grape::Entity
- expose :expired?, as: :expired
- expose :expiration
- end
-
- class PagesDomainCertificate < Grape::Entity
- expose :subject
- expose :expired?, as: :expired
- expose :certificate
- expose :certificate_text
- end
-
- class PagesDomainBasic < Grape::Entity
- expose :domain
- expose :url
- expose :project_id
- expose :verified?, as: :verified
- expose :verification_code, as: :verification_code
- expose :enabled_until
- expose :auto_ssl_enabled
-
- expose :certificate,
- as: :certificate_expiration,
- if: ->(pages_domain, _) { pages_domain.certificate? },
- using: PagesDomainCertificateExpiration do |pages_domain|
- pages_domain
- end
- end
-
- class PagesDomain < Grape::Entity
- expose :domain
- expose :url
- expose :verified?, as: :verified
- expose :verification_code, as: :verification_code
- expose :enabled_until
- expose :auto_ssl_enabled
-
- expose :certificate,
- if: ->(pages_domain, _) { pages_domain.certificate? },
- using: PagesDomainCertificate do |pages_domain|
- pages_domain
- end
- end
-
- class Application < Grape::Entity
- expose :id
- expose :uid, as: :application_id
- expose :name, as: :application_name
- expose :redirect_uri, as: :callback_url
- end
-
- # Use with care, this exposes the secret
- class ApplicationWithSecret < Application
- expose :secret
- end
-
- class Blob < Grape::Entity
- expose :basename
- expose :data
- expose :path
- # TODO: :filename was renamed to :path but both still return the full path,
- # in the future we can only return the filename here without the leading
- # directory path.
- # https://gitlab.com/gitlab-org/gitlab/issues/34521
- expose :filename, &:path
- expose :id
- expose :ref
- expose :startline
- expose :project_id
- end
-
- class BasicBadgeDetails < Grape::Entity
- expose :name
- expose :link_url
- expose :image_url
- expose :rendered_link_url do |badge, options|
- badge.rendered_link_url(options.fetch(:project, nil))
- end
- expose :rendered_image_url do |badge, options|
- badge.rendered_image_url(options.fetch(:project, nil))
- end
- end
-
- class Badge < BasicBadgeDetails
- expose :id
- expose :kind do |badge|
- badge.type == 'ProjectBadge' ? 'project' : 'group'
- end
- end
-
- class ResourceLabelEvent < Grape::Entity
- expose :id
- expose :user, using: Entities::UserBasic
- expose :created_at
- expose :resource_type do |event, options|
- event.issuable.class.name
- end
- expose :resource_id do |event, options|
- event.issuable.id
- end
- expose :label, using: Entities::LabelBasic
- expose :action
- end
-
- class Suggestion < Grape::Entity
- expose :id
- expose :from_line
- expose :to_line
- expose :appliable?, as: :appliable
- expose :applied
- expose :from_content
- expose :to_content
- end
-
- module Platform
- class Kubernetes < Grape::Entity
- expose :api_url
- expose :namespace
- expose :authorization_type
- expose :ca_cert
- end
- end
-
- module Provider
- class Gcp < Grape::Entity
- expose :cluster_id
- expose :status_name
- expose :gcp_project_id
- expose :zone
- expose :machine_type
- expose :num_nodes
- expose :endpoint
- end
- end
-
- class Cluster < Grape::Entity
- expose :id, :name, :created_at, :domain
- expose :provider_type, :platform_type, :environment_scope, :cluster_type
- expose :user, using: Entities::UserBasic
- expose :platform_kubernetes, using: Entities::Platform::Kubernetes
- expose :provider_gcp, using: Entities::Provider::Gcp
- expose :management_project, using: Entities::ProjectIdentity
- end
-
- class ClusterProject < Cluster
- expose :project, using: Entities::BasicProjectDetails
- end
-
- class ClusterGroup < Cluster
- expose :group, using: Entities::BasicGroupDetails
- end
-
- module InternalPostReceive
- class Message < Grape::Entity
- expose :message
- expose :type
- end
-
- class Response < Grape::Entity
- expose :messages, using: Message
- expose :reference_counter_decreased
- end
- end
- end
-end
-
-# rubocop: disable Cop/InjectEnterpriseEditionModule
-::API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting')
-::API::Entities::Board.prepend_if_ee('EE::API::Entities::Board')
-::API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true)
-::API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail')
-::API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true)
-::API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue')
-::API::Entities::List.prepend_if_ee('EE::API::Entities::List')
-::API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true)
-::API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true)
-::API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace')
-::API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true)
-::API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess')
-::API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true)
-::API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo')
-::API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch')
-::API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity')
-::API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true)
diff --git a/lib/api/entities/access_requester.rb b/lib/api/entities/access_requester.rb
new file mode 100644
index 00000000000..951250225cc
--- /dev/null
+++ b/lib/api/entities/access_requester.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class AccessRequester < Grape::Entity
+ expose :user, merge: true, using: UserBasic
+ expose :requested_at
+ end
+ end
+end
diff --git a/lib/api/entities/appearance.rb b/lib/api/entities/appearance.rb
new file mode 100644
index 00000000000..c3cffc8d05c
--- /dev/null
+++ b/lib/api/entities/appearance.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Appearance < Grape::Entity
+ expose :title
+ expose :description
+
+ expose :logo do |appearance, options|
+ appearance.logo.url
+ end
+
+ expose :header_logo do |appearance, options|
+ appearance.header_logo.url
+ end
+
+ expose :favicon do |appearance, options|
+ appearance.favicon.url
+ end
+
+ expose :new_project_guidelines
+ expose :header_message
+ expose :footer_message
+ expose :message_background_color
+ expose :message_font_color
+ expose :email_header_and_footer_enabled
+ end
+ end
+end
diff --git a/lib/api/entities/application.rb b/lib/api/entities/application.rb
new file mode 100644
index 00000000000..33514200424
--- /dev/null
+++ b/lib/api/entities/application.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Application < Grape::Entity
+ expose :id
+ expose :uid, as: :application_id
+ expose :name, as: :application_name
+ expose :redirect_uri, as: :callback_url
+ expose :confidential
+ end
+ end
+end
diff --git a/lib/api/entities/application_setting.rb b/lib/api/entities/application_setting.rb
new file mode 100644
index 00000000000..e9572a8d430
--- /dev/null
+++ b/lib/api/entities/application_setting.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ApplicationSetting < Grape::Entity
+ def self.exposed_attributes
+ attributes = ::ApplicationSettingsHelper.visible_attributes
+ attributes.delete(:performance_bar_allowed_group_path)
+ attributes.delete(:performance_bar_enabled)
+ attributes.delete(:allow_local_requests_from_hooks_and_services)
+
+ # let's not expose the secret key in a response
+ attributes.delete(:asset_proxy_secret_key)
+ attributes.delete(:eks_secret_access_key)
+
+ attributes
+ end
+
+ expose :id, :performance_bar_allowed_group_id
+ expose(*exposed_attributes)
+ expose(:restricted_visibility_levels) do |setting, _options|
+ setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) }
+ end
+ expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
+ expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
+ expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
+
+ expose(*::ApplicationSettingsHelper.external_authorization_service_attributes)
+
+ # support legacy names, can be removed in v5
+ expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
+ expose :password_authentication_enabled_for_web, as: :signin_enabled
+ expose :allow_local_requests_from_web_hooks_and_services, as: :allow_local_requests_from_hooks_and_services
+ end
+ end
+end
+
+API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting')
diff --git a/lib/api/entities/application_statistics.rb b/lib/api/entities/application_statistics.rb
new file mode 100644
index 00000000000..4bcba1da464
--- /dev/null
+++ b/lib/api/entities/application_statistics.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ApplicationStatistics < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+ include CountHelper
+
+ expose :forks do |counts|
+ approximate_fork_count_with_delimiters(counts)
+ end
+
+ expose :issues do |counts|
+ approximate_count_with_delimiters(counts, ::Issue)
+ end
+
+ expose :merge_requests do |counts|
+ approximate_count_with_delimiters(counts, ::MergeRequest)
+ end
+
+ expose :notes do |counts|
+ approximate_count_with_delimiters(counts, ::Note)
+ end
+
+ expose :snippets do |counts|
+ approximate_count_with_delimiters(counts, ::Snippet)
+ end
+
+ expose :ssh_keys do |counts|
+ approximate_count_with_delimiters(counts, ::Key)
+ end
+
+ expose :milestones do |counts|
+ approximate_count_with_delimiters(counts, ::Milestone)
+ end
+
+ expose :users do |counts|
+ approximate_count_with_delimiters(counts, ::User)
+ end
+
+ expose :projects do |counts|
+ approximate_count_with_delimiters(counts, ::Project)
+ end
+
+ expose :groups do |counts|
+ approximate_count_with_delimiters(counts, ::Group)
+ end
+
+ expose :active_users do |_|
+ number_with_delimiter(::User.active.count)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/application_with_secret.rb b/lib/api/entities/application_with_secret.rb
new file mode 100644
index 00000000000..3e540381d89
--- /dev/null
+++ b/lib/api/entities/application_with_secret.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ # Use with care, this exposes the secret
+ class ApplicationWithSecret < Entities::Application
+ expose :secret
+ end
+ end
+end
diff --git a/lib/api/entities/avatar.rb b/lib/api/entities/avatar.rb
new file mode 100644
index 00000000000..7d5c762afcc
--- /dev/null
+++ b/lib/api/entities/avatar.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Avatar < Grape::Entity
+ expose :avatar_url do |avatarable, options|
+ avatarable.avatar_url(only_path: false, size: options[:size])
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/award_emoji.rb b/lib/api/entities/award_emoji.rb
new file mode 100644
index 00000000000..da9a183bf39
--- /dev/null
+++ b/lib/api/entities/award_emoji.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
+ end
+ end
+end
diff --git a/lib/api/entities/badge.rb b/lib/api/entities/badge.rb
new file mode 100644
index 00000000000..1e3e2ec469a
--- /dev/null
+++ b/lib/api/entities/badge.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Badge < Entities::BasicBadgeDetails
+ expose :id
+ expose :kind do |badge|
+ badge.type == 'ProjectBadge' ? 'project' : 'group'
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/basic_badge_details.rb b/lib/api/entities/basic_badge_details.rb
new file mode 100644
index 00000000000..273dc57fe67
--- /dev/null
+++ b/lib/api/entities/basic_badge_details.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BasicBadgeDetails < Grape::Entity
+ expose :name
+ expose :link_url
+ expose :image_url
+ expose :rendered_link_url do |badge, options|
+ badge.rendered_link_url(options.fetch(:project, nil))
+ end
+ expose :rendered_image_url do |badge, options|
+ badge.rendered_image_url(options.fetch(:project, nil))
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/basic_group_details.rb b/lib/api/entities/basic_group_details.rb
new file mode 100644
index 00000000000..882fce4ef2c
--- /dev/null
+++ b/lib/api/entities/basic_group_details.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BasicGroupDetails < Grape::Entity
+ expose :id
+ expose :web_url
+ expose :name
+ end
+ end
+end
diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb
new file mode 100644
index 00000000000..13bc19456b3
--- /dev/null
+++ b/lib/api/entities/basic_project_details.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BasicProjectDetails < Entities::ProjectIdentity
+ include ::API::ProjectsRelationBuilder
+
+ expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
+ expose :tag_list do |project|
+ # project.tags.order(:name).pluck(:name) is the most suitable option
+ # to avoid loading all the ActiveRecord objects but, if we use it here
+ # it override the preloaded associations and makes a query
+ # (fixed in https://github.com/rails/rails/pull/25976).
+ project.tags.map(&:name).sort
+ end
+
+ expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
+
+ expose :license_url, if: :license do |project|
+ license = project.repository.license_blob
+
+ if license
+ Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path))
+ end
+ end
+
+ expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project|
+ project.repository.license
+ end
+
+ expose :avatar_url do |project, options|
+ project.avatar_url(only_path: false)
+ end
+
+ expose :star_count, :forks_count
+ expose :last_activity_at
+ expose :namespace, using: 'API::Entities::NamespaceBasic'
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.preload_relation(projects_relation, options = {})
+ # Preloading tags, should be done with using only `:tags`,
+ # as `:tags` are defined as: `has_many :tags, through: :taggings`
+ # N+1 is solved then by using `subject.tags.map(&:name)`
+ # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555
+ projects_relation.preload(:project_feature, :route)
+ .preload(:import_state, :tags)
+ .preload(:auto_devops)
+ .preload(namespace: [:route, :owner])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/api/entities/basic_ref.rb b/lib/api/entities/basic_ref.rb
new file mode 100644
index 00000000000..79c15075d99
--- /dev/null
+++ b/lib/api/entities/basic_ref.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BasicRef < Grape::Entity
+ expose :type, :name
+ end
+ end
+end
diff --git a/lib/api/entities/blame_range.rb b/lib/api/entities/blame_range.rb
new file mode 100644
index 00000000000..20d09c15278
--- /dev/null
+++ b/lib/api/entities/blame_range.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BlameRange < Grape::Entity
+ expose :commit, using: BlameRangeCommit
+ expose :lines
+ end
+ end
+end
diff --git a/lib/api/entities/blame_range_commit.rb b/lib/api/entities/blame_range_commit.rb
new file mode 100644
index 00000000000..3c1958925d7
--- /dev/null
+++ b/lib/api/entities/blame_range_commit.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BlameRangeCommit < Grape::Entity
+ expose :id
+ expose :parent_ids
+ expose :message
+ expose :authored_date, :author_name, :author_email
+ expose :committed_date, :committer_name, :committer_email
+ end
+ end
+end
diff --git a/lib/api/entities/blob.rb b/lib/api/entities/blob.rb
new file mode 100644
index 00000000000..b14ef127b68
--- /dev/null
+++ b/lib/api/entities/blob.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Blob < Grape::Entity
+ expose :basename
+ expose :data
+ expose :path
+ # TODO: :filename was renamed to :path but both still return the full path,
+ # in the future we can only return the filename here without the leading
+ # directory path.
+ # https://gitlab.com/gitlab-org/gitlab/issues/34521
+ expose :filename, &:path
+ expose :id
+ expose :ref
+ expose :startline
+ expose :project_id
+ end
+ end
+end
diff --git a/lib/api/entities/board.rb b/lib/api/entities/board.rb
new file mode 100644
index 00000000000..5bb1cde0fa9
--- /dev/null
+++ b/lib/api/entities/board.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Board < Grape::Entity
+ expose :id
+ expose :project, using: Entities::BasicProjectDetails
+
+ expose :lists, using: Entities::List do |board|
+ board.destroyable_lists
+ end
+ end
+ end
+end
+
+API::Entities::Board.prepend_if_ee('EE::API::Entities::Board')
diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb
new file mode 100644
index 00000000000..1d5017ac702
--- /dev/null
+++ b/lib/api/entities/branch.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Branch < Grape::Entity
+ expose :name
+
+ expose :commit, using: Entities::Commit do |repo_branch, options|
+ options[:project].repository.commit(repo_branch.dereferenced_target)
+ end
+
+ expose :merged do |repo_branch, options|
+ if options[:merged_branch_names]
+ options[:merged_branch_names].include?(repo_branch.name)
+ else
+ options[:project].repository.merged_to_root_ref?(repo_branch)
+ end
+ end
+
+ expose :protected do |repo_branch, options|
+ ::ProtectedBranch.protected?(options[:project], repo_branch.name)
+ end
+
+ expose :developers_can_push do |repo_branch, options|
+ ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
+ end
+
+ expose :developers_can_merge do |repo_branch, options|
+ ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
+ end
+
+ expose :can_push do |repo_branch, options|
+ Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name)
+ end
+
+ expose :default do |repo_branch, options|
+ options[:project].default_branch == repo_branch.name
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/broadcast_message.rb b/lib/api/entities/broadcast_message.rb
new file mode 100644
index 00000000000..403677aa300
--- /dev/null
+++ b/lib/api/entities/broadcast_message.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class BroadcastMessage < Grape::Entity
+ expose :id, :message, :starts_at, :ends_at, :color, :font, :target_path, :broadcast_type
+ expose :active?, as: :active
+ end
+ end
+end
diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb
new file mode 100644
index 00000000000..4cb54e988ce
--- /dev/null
+++ b/lib/api/entities/cluster.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Cluster < Grape::Entity
+ expose :id, :name, :created_at, :domain
+ expose :provider_type, :platform_type, :environment_scope, :cluster_type
+ expose :user, using: Entities::UserBasic
+ expose :platform_kubernetes, using: Entities::Platform::Kubernetes
+ expose :provider_gcp, using: Entities::Provider::Gcp
+ expose :management_project, using: Entities::ProjectIdentity
+ end
+ end
+end
diff --git a/lib/api/entities/cluster_group.rb b/lib/api/entities/cluster_group.rb
new file mode 100644
index 00000000000..8f71438cf3d
--- /dev/null
+++ b/lib/api/entities/cluster_group.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ClusterGroup < Entities::Cluster
+ expose :group, using: Entities::BasicGroupDetails
+ end
+ end
+end
diff --git a/lib/api/entities/cluster_project.rb b/lib/api/entities/cluster_project.rb
new file mode 100644
index 00000000000..2fd3e35e2a2
--- /dev/null
+++ b/lib/api/entities/cluster_project.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ClusterProject < Entities::Cluster
+ expose :project, using: Entities::BasicProjectDetails
+ end
+ end
+end
diff --git a/lib/api/entities/commit.rb b/lib/api/entities/commit.rb
new file mode 100644
index 00000000000..7ce97c2c3d8
--- /dev/null
+++ b/lib/api/entities/commit.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Commit < Grape::Entity
+ expose :id, :short_id, :created_at
+ expose :parent_ids
+ expose :full_title, as: :title
+ expose :safe_message, as: :message
+ expose :author_name, :author_email, :authored_date
+ expose :committer_name, :committer_email, :committed_date
+ end
+ end
+end
diff --git a/lib/api/entities/commit_detail.rb b/lib/api/entities/commit_detail.rb
new file mode 100644
index 00000000000..22424b38bb9
--- /dev/null
+++ b/lib/api/entities/commit_detail.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitDetail < Commit
+ expose :stats, using: Entities::CommitStats, if: :stats
+ expose :status
+ expose :project_id
+
+ expose :last_pipeline do |commit, options|
+ pipeline = commit.last_pipeline if can_read_pipeline?
+ ::API::Entities::PipelineBasic.represent(pipeline, options)
+ end
+
+ private
+
+ def can_read_pipeline?
+ Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/commit_note.rb b/lib/api/entities/commit_note.rb
new file mode 100644
index 00000000000..d08b6fc8225
--- /dev/null
+++ b/lib/api/entities/commit_note.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitNote < Grape::Entity
+ expose :note
+ expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? }
+ expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? }
+ expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? }
+ expose :author, using: Entities::UserBasic
+ expose :created_at
+ end
+ end
+end
diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb
new file mode 100644
index 00000000000..8e86d4c1aa6
--- /dev/null
+++ b/lib/api/entities/commit_signature.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitSignature < Grape::Entity
+ expose :gpg_key_id
+ expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email
+ expose :verification_status
+ expose :gpg_key_subkey_id
+ end
+ end
+end
diff --git a/lib/api/entities/commit_stats.rb b/lib/api/entities/commit_stats.rb
new file mode 100644
index 00000000000..d9ba99c8eb0
--- /dev/null
+++ b/lib/api/entities/commit_stats.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitStats < Grape::Entity
+ expose :additions, :deletions, :total
+ end
+ end
+end
diff --git a/lib/api/entities/commit_status.rb b/lib/api/entities/commit_status.rb
new file mode 100644
index 00000000000..61b8bf89cfe
--- /dev/null
+++ b/lib/api/entities/commit_status.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitStatus < Grape::Entity
+ expose :id, :sha, :ref, :status, :name, :target_url, :description,
+ :created_at, :started_at, :finished_at, :allow_failure, :coverage
+ expose :author, using: Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/commit_with_stats.rb b/lib/api/entities/commit_with_stats.rb
new file mode 100644
index 00000000000..8a992586e22
--- /dev/null
+++ b/lib/api/entities/commit_with_stats.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CommitWithStats < Commit
+ expose :stats, using: Entities::CommitStats
+ end
+ end
+end
diff --git a/lib/api/entities/compare.rb b/lib/api/entities/compare.rb
new file mode 100644
index 00000000000..fe2f03db2af
--- /dev/null
+++ b/lib/api/entities/compare.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Compare < Grape::Entity
+ expose :commit, using: Entities::Commit do |compare, _|
+ compare.commits.last
+ end
+
+ expose :commits, using: Entities::Commit do |compare, _|
+ compare.commits
+ end
+
+ expose :diffs, using: Entities::Diff do |compare, _|
+ compare.diffs.diffs.to_a
+ end
+
+ expose :compare_timeout do |compare, _|
+ compare.diffs.diffs.overflow?
+ end
+
+ expose :same, as: :compare_same_ref
+ end
+ end
+end
diff --git a/lib/api/entities/container_expiration_policy.rb b/lib/api/entities/container_expiration_policy.rb
new file mode 100644
index 00000000000..853bbb9b76b
--- /dev/null
+++ b/lib/api/entities/container_expiration_policy.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ContainerExpirationPolicy < Grape::Entity
+ expose :cadence
+ expose :enabled
+ expose :keep_n
+ expose :older_than
+ expose :name_regex
+ expose :next_run_at
+ end
+ end
+end
diff --git a/lib/api/entities/contributor.rb b/lib/api/entities/contributor.rb
new file mode 100644
index 00000000000..8763822b674
--- /dev/null
+++ b/lib/api/entities/contributor.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Contributor < Grape::Entity
+ expose :name, :email, :commits, :additions, :deletions
+ end
+ end
+end
diff --git a/lib/api/entities/custom_attribute.rb b/lib/api/entities/custom_attribute.rb
new file mode 100644
index 00000000000..f949b709517
--- /dev/null
+++ b/lib/api/entities/custom_attribute.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class CustomAttribute < Grape::Entity
+ expose :key
+ expose :value
+ end
+ end
+end
diff --git a/lib/api/entities/deploy_key_with_user.rb b/lib/api/entities/deploy_key_with_user.rb
new file mode 100644
index 00000000000..31024dc3910
--- /dev/null
+++ b/lib/api/entities/deploy_key_with_user.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class DeployKeyWithUser < Entities::SSHKeyWithUser
+ expose :deploy_keys_projects
+ end
+ end
+end
diff --git a/lib/api/entities/deploy_keys_project.rb b/lib/api/entities/deploy_keys_project.rb
new file mode 100644
index 00000000000..64725459167
--- /dev/null
+++ b/lib/api/entities/deploy_keys_project.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class DeployKeysProject < Grape::Entity
+ expose :deploy_key, merge: true, using: Entities::SSHKey
+ expose :can_push
+ end
+ end
+end
diff --git a/lib/api/entities/deployment.rb b/lib/api/entities/deployment.rb
new file mode 100644
index 00000000000..3a97d3e3c09
--- /dev/null
+++ b/lib/api/entities/deployment.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at, :updated_at
+ expose :user, using: Entities::UserBasic
+ expose :environment, using: Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Job
+ expose :status
+ end
+ end
+end
diff --git a/lib/api/entities/diff.rb b/lib/api/entities/diff.rb
new file mode 100644
index 00000000000..e92bc5d6b68
--- /dev/null
+++ b/lib/api/entities/diff.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Diff < Grape::Entity
+ expose :old_path, :new_path, :a_mode, :b_mode
+ expose :new_file?, as: :new_file
+ expose :renamed_file?, as: :renamed_file
+ expose :deleted_file?, as: :deleted_file
+ expose :json_safe_diff, as: :diff
+ end
+ end
+end
diff --git a/lib/api/entities/diff_position.rb b/lib/api/entities/diff_position.rb
new file mode 100644
index 00000000000..10150d04ac8
--- /dev/null
+++ b/lib/api/entities/diff_position.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class DiffPosition < Grape::Entity
+ expose :base_sha, :start_sha, :head_sha, :old_path, :new_path,
+ :position_type
+ end
+ end
+end
diff --git a/lib/api/entities/diff_refs.rb b/lib/api/entities/diff_refs.rb
new file mode 100644
index 00000000000..8772fa2334f
--- /dev/null
+++ b/lib/api/entities/diff_refs.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class DiffRefs < Grape::Entity
+ expose :base_sha, :head_sha, :start_sha
+ end
+ end
+end
diff --git a/lib/api/entities/discussion.rb b/lib/api/entities/discussion.rb
new file mode 100644
index 00000000000..dd1dd40da23
--- /dev/null
+++ b/lib/api/entities/discussion.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Discussion < Grape::Entity
+ expose :id
+ expose :individual_note?, as: :individual_note
+ expose :notes, using: Entities::Note
+ end
+ end
+end
diff --git a/lib/api/entities/email.rb b/lib/api/entities/email.rb
new file mode 100644
index 00000000000..5ba425def3d
--- /dev/null
+++ b/lib/api/entities/email.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Email < Grape::Entity
+ expose :id, :email
+ end
+ end
+end
diff --git a/lib/api/entities/environment.rb b/lib/api/entities/environment.rb
new file mode 100644
index 00000000000..cb39ce1b13a
--- /dev/null
+++ b/lib/api/entities/environment.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Environment < Entities::EnvironmentBasic
+ expose :project, using: Entities::BasicProjectDetails
+ expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
+ expose :state
+ end
+ end
+end
diff --git a/lib/api/entities/environment_basic.rb b/lib/api/entities/environment_basic.rb
new file mode 100644
index 00000000000..061d4739874
--- /dev/null
+++ b/lib/api/entities/environment_basic.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class EnvironmentBasic < Grape::Entity
+ expose :id, :name, :slug, :external_url
+ end
+ end
+end
diff --git a/lib/api/entities/event.rb b/lib/api/entities/event.rb
new file mode 100644
index 00000000000..9c2d766b7f1
--- /dev/null
+++ b/lib/api/entities/event.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Event < Grape::Entity
+ expose :project_id, :action_name
+ expose :target_id, :target_iid, :target_type, :author_id
+ expose :target_title
+ expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
+
+ expose :push_event_payload,
+ as: :push_data,
+ using: Entities::PushEventPayload,
+ if: -> (event, _) { event.push_action? }
+
+ expose :author_username do |event, options|
+ event.author&.username
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/external_issue.rb b/lib/api/entities/external_issue.rb
new file mode 100644
index 00000000000..8a201f70099
--- /dev/null
+++ b/lib/api/entities/external_issue.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ExternalIssue < Grape::Entity
+ expose :title
+ expose :id
+ end
+ end
+end
diff --git a/lib/api/entities/feature.rb b/lib/api/entities/feature.rb
new file mode 100644
index 00000000000..3c9182340ea
--- /dev/null
+++ b/lib/api/entities/feature.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Feature < Grape::Entity
+ expose :name
+ expose :state
+ expose :gates, using: Entities::FeatureGate do |model|
+ model.gates.map do |gate|
+ value = model.gate_values[gate.key]
+
+ # By default all gate values are populated. Only show relevant ones.
+ if (value.is_a?(Integer) && value.zero?) || (value.is_a?(Set) && value.empty?)
+ next
+ end
+
+ { key: gate.key, value: value }
+ end.compact
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/feature_gate.rb b/lib/api/entities/feature_gate.rb
new file mode 100644
index 00000000000..bea9c9474b3
--- /dev/null
+++ b/lib/api/entities/feature_gate.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class FeatureGate < Grape::Entity
+ expose :key
+ expose :value
+ end
+ end
+end
diff --git a/lib/api/entities/global_notification_setting.rb b/lib/api/entities/global_notification_setting.rb
new file mode 100644
index 00000000000..f3ca64347f0
--- /dev/null
+++ b/lib/api/entities/global_notification_setting.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GlobalNotificationSetting < Entities::NotificationSetting
+ expose :notification_email do |notification_setting, options|
+ notification_setting.user.notification_email
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/gpg_key.rb b/lib/api/entities/gpg_key.rb
new file mode 100644
index 00000000000..a97e704a5dd
--- /dev/null
+++ b/lib/api/entities/gpg_key.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GPGKey < Grape::Entity
+ expose :id, :key, :created_at
+ end
+ end
+end
diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb
new file mode 100644
index 00000000000..ae5ee4784ed
--- /dev/null
+++ b/lib/api/entities/group.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Group < BasicGroupDetails
+ expose :path, :description, :visibility
+ expose :share_with_group_lock
+ expose :require_two_factor_authentication
+ expose :two_factor_grace_period
+ expose :project_creation_level_str, as: :project_creation_level
+ expose :auto_devops_enabled
+ expose :subgroup_creation_level_str, as: :subgroup_creation_level
+ expose :emails_disabled
+ expose :mentions_disabled
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :avatar_url do |group, options|
+ group.avatar_url(only_path: false)
+ end
+ expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
+
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
+
+ expose :statistics, if: :statistics do
+ with_options format_with: -> (value) { value.to_i } do
+ expose :storage_size
+ expose :repository_size
+ expose :wiki_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size, as: :job_artifacts_size
+ end
+ end
+ end
+ end
+end
+
+API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true)
diff --git a/lib/api/entities/group_access.rb b/lib/api/entities/group_access.rb
new file mode 100644
index 00000000000..5e53e9645c2
--- /dev/null
+++ b/lib/api/entities/group_access.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GroupAccess < MemberAccess
+ end
+ end
+end
diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb
new file mode 100644
index 00000000000..e03047a6e75
--- /dev/null
+++ b/lib/api/entities/group_detail.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GroupDetail < Group
+ expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] }
+ expose :projects, using: Entities::Project do |group, options|
+ projects = GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_owned: true, limit: projects_limit }
+ ).execute
+
+ Entities::Project.prepare_relation(projects)
+ end
+
+ expose :shared_projects, using: Entities::Project do |group, options|
+ projects = GroupProjectsFinder.new(
+ group: group,
+ current_user: options[:current_user],
+ options: { only_shared: true, limit: projects_limit }
+ ).execute
+
+ Entities::Project.prepare_relation(projects)
+ end
+
+ def projects_limit
+ if ::Feature.enabled?(:limit_projects_in_groups_api, default_enabled: true)
+ GroupProjectsFinder::DEFAULT_PROJECTS_LIMIT
+ else
+ nil
+ end
+ end
+ end
+ end
+end
+
+API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail')
diff --git a/lib/api/entities/group_label.rb b/lib/api/entities/group_label.rb
new file mode 100644
index 00000000000..4e1b9226e6d
--- /dev/null
+++ b/lib/api/entities/group_label.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GroupLabel < Entities::Label
+ end
+ end
+end
diff --git a/lib/api/entities/hook.rb b/lib/api/entities/hook.rb
new file mode 100644
index 00000000000..ac813bcac3f
--- /dev/null
+++ b/lib/api/entities/hook.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Hook < Grape::Entity
+ expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events
+ expose :enable_ssl_verification
+ end
+ end
+end
diff --git a/lib/api/entities/identity.rb b/lib/api/entities/identity.rb
new file mode 100644
index 00000000000..52045b6250a
--- /dev/null
+++ b/lib/api/entities/identity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Identity < Grape::Entity
+ expose :provider, :extern_uid
+ end
+ end
+end
+
+API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity')
diff --git a/lib/api/entities/impersonation_token.rb b/lib/api/entities/impersonation_token.rb
new file mode 100644
index 00000000000..9ee8f8bf77b
--- /dev/null
+++ b/lib/api/entities/impersonation_token.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ImpersonationToken < Entities::PersonalAccessToken
+ expose :impersonation
+ end
+ end
+end
diff --git a/lib/api/entities/impersonation_token_with_token.rb b/lib/api/entities/impersonation_token_with_token.rb
new file mode 100644
index 00000000000..4904f107628
--- /dev/null
+++ b/lib/api/entities/impersonation_token_with_token.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ImpersonationTokenWithToken < Entities::PersonalAccessTokenWithToken
+ expose :impersonation
+ end
+ end
+end
diff --git a/lib/api/entities/internal_post_receive/message.rb b/lib/api/entities/internal_post_receive/message.rb
new file mode 100644
index 00000000000..3cfefa84d9b
--- /dev/null
+++ b/lib/api/entities/internal_post_receive/message.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module InternalPostReceive
+ class Message < Grape::Entity
+ expose :message
+ expose :type
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/internal_post_receive/response.rb b/lib/api/entities/internal_post_receive/response.rb
new file mode 100644
index 00000000000..c33418ed658
--- /dev/null
+++ b/lib/api/entities/internal_post_receive/response.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module InternalPostReceive
+ class Response < Grape::Entity
+ expose :messages, using: Entities::InternalPostReceive::Message
+ expose :reference_counter_decreased
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb
new file mode 100644
index 00000000000..5bee59de539
--- /dev/null
+++ b/lib/api/entities/issuable_entity.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssuableEntity < Grape::Entity
+ expose :id, :iid
+ expose(:project_id) { |entity| entity&.project.try(:id) }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+
+ # Avoids an N+1 query when metadata is included
+ def issuable_metadata(subject, options, method, args = nil)
+ cached_subject = options.dig(:issuable_metadata, subject.id)
+ (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/issuable_references.rb b/lib/api/entities/issuable_references.rb
new file mode 100644
index 00000000000..1bf078847cf
--- /dev/null
+++ b/lib/api/entities/issuable_references.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssuableReferences < Grape::Entity
+ expose :short do |issuable|
+ issuable.to_reference
+ end
+
+ expose :relative do |issuable, options|
+ issuable.to_reference(options[:group] || options[:project])
+ end
+
+ expose :full do |issuable|
+ issuable.to_reference(full: true)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/issuable_time_stats.rb b/lib/api/entities/issuable_time_stats.rb
new file mode 100644
index 00000000000..7c3452a10a1
--- /dev/null
+++ b/lib/api/entities/issuable_time_stats.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssuableTimeStats < Grape::Entity
+ format_with(:time_tracking_formatter) do |time_spent|
+ Gitlab::TimeTrackingFormatter.output(time_spent)
+ end
+
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+
+ with_options(format_with: :time_tracking_formatter) do
+ expose :total_time_spent, as: :human_total_time_spent
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def total_time_spent
+ # Avoids an N+1 query since timelogs are preloaded
+ object.timelogs.map(&:time_spent).sum
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb
new file mode 100644
index 00000000000..5f2609cf68b
--- /dev/null
+++ b/lib/api/entities/issue.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Issue < IssueBasic
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose(:has_tasks) do |issue, _|
+ !issue.task_list_items.empty?
+ end
+
+ expose :task_status, if: -> (issue, _) do
+ !issue.task_list_items.empty?
+ end
+
+ expose :_links do
+ expose :self do |issue|
+ expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :notes do |issue|
+ expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
+ end
+
+ expose :award_emoji do |issue|
+ expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :project do |issue|
+ expose_url(api_v4_projects_path(id: issue.project_id))
+ end
+ end
+
+ expose :references, with: IssuableReferences do |issue|
+ issue
+ end
+
+ # Calculating the value of subscribed field triggers Markdown
+ # processing. We can't do that for multiple issues / merge
+ # requests in a single API request.
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
+
+ expose :moved_to_id
+ end
+ end
+end
+
+API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue')
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
new file mode 100644
index 00000000000..af92f4124f1
--- /dev/null
+++ b/lib/api/entities/issue_basic.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class IssueBasic < IssuableEntity
+ expose :closed_at
+ expose :closed_by, using: Entities::UserBasic
+
+ expose :labels do |issue, options|
+ if options[:with_labels_details]
+ ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
+ else
+ issue.labels.map(&:title).sort
+ end
+ end
+
+ expose :milestone, using: Entities::Milestone
+ expose :assignees, :author, using: Entities::UserBasic
+
+ expose :assignee, using: ::API::Entities::UserBasic do |issue|
+ issue.assignees.first
+ end
+
+ expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
+ expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
+ expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
+ expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
+ expose :due_date
+ expose :confidential
+ expose :discussion_locked
+
+ expose :web_url do |issue|
+ Gitlab::UrlBuilder.build(issue)
+ end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
+ issue
+ end
+
+ expose :task_completion_status
+ end
+ end
+end
+
+API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true)
diff --git a/lib/api/entities/job.rb b/lib/api/entities/job.rb
new file mode 100644
index 00000000000..cbee8794007
--- /dev/null
+++ b/lib/api/entities/job.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Job < Entities::JobBasic
+ # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
+ expose :artifacts_file, using: Entities::JobArtifactFile, if: -> (job, opts) { job.artifacts? }
+ expose :job_artifacts, as: :artifacts, using: Entities::JobArtifact
+ expose :runner, with: Entities::Runner
+ expose :artifacts_expire_at
+ end
+ end
+end
diff --git a/lib/api/entities/job_artifact.rb b/lib/api/entities/job_artifact.rb
new file mode 100644
index 00000000000..94dbdb38fee
--- /dev/null
+++ b/lib/api/entities/job_artifact.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class JobArtifact < Grape::Entity
+ expose :file_type, :size, :filename, :file_format
+ end
+ end
+end
diff --git a/lib/api/entities/job_artifact_file.rb b/lib/api/entities/job_artifact_file.rb
new file mode 100644
index 00000000000..fa2851a7f0e
--- /dev/null
+++ b/lib/api/entities/job_artifact_file.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class JobArtifactFile < Grape::Entity
+ expose :filename
+ expose :cached_size, as: :size
+ end
+ end
+end
diff --git a/lib/api/entities/job_basic.rb b/lib/api/entities/job_basic.rb
new file mode 100644
index 00000000000..a8541039934
--- /dev/null
+++ b/lib/api/entities/job_basic.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class JobBasic < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
+ expose :created_at, :started_at, :finished_at
+ expose :duration
+ expose :user, with: Entities::User
+ expose :commit, with: Entities::Commit
+ expose :pipeline, with: Entities::PipelineBasic
+
+ expose :web_url do |job, _options|
+ Gitlab::Routing.url_helpers.project_job_url(job.project, job)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_basic_with_project.rb b/lib/api/entities/job_basic_with_project.rb
new file mode 100644
index 00000000000..09387e045ec
--- /dev/null
+++ b/lib/api/entities/job_basic_with_project.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class JobBasicWithProject < Entities::JobBasic
+ expose :project, with: Entities::ProjectIdentity
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/artifacts.rb b/lib/api/entities/job_request/artifacts.rb
new file mode 100644
index 00000000000..c6871fdd875
--- /dev/null
+++ b/lib/api/entities/job_request/artifacts.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Artifacts < Grape::Entity
+ expose :name
+ expose :untracked
+ expose :paths
+ expose :when
+ expose :expire_in
+ expose :artifact_type
+ expose :artifact_format
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb
new file mode 100644
index 00000000000..a75affbaf84
--- /dev/null
+++ b/lib/api/entities/job_request/cache.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Cache < Grape::Entity
+ expose :key, :untracked, :paths, :policy
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/credentials.rb b/lib/api/entities/job_request/credentials.rb
new file mode 100644
index 00000000000..cdac5566cbd
--- /dev/null
+++ b/lib/api/entities/job_request/credentials.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Credentials < Grape::Entity
+ expose :type, :url, :username, :password
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb
new file mode 100644
index 00000000000..64d779f6575
--- /dev/null
+++ b/lib/api/entities/job_request/dependency.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Dependency < Grape::Entity
+ expose :id, :name, :token
+ expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? }
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/git_info.rb b/lib/api/entities/job_request/git_info.rb
new file mode 100644
index 00000000000..e07099263b5
--- /dev/null
+++ b/lib/api/entities/job_request/git_info.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class GitInfo < Grape::Entity
+ expose :repo_url, :ref, :sha, :before_sha
+ expose :ref_type
+ expose :refspecs
+ expose :git_depth, as: :depth
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/image.rb b/lib/api/entities/job_request/image.rb
new file mode 100644
index 00000000000..47f4542d2d5
--- /dev/null
+++ b/lib/api/entities/job_request/image.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Image < Grape::Entity
+ expose :name, :entrypoint
+ expose :ports, using: Entities::JobRequest::Port
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb
new file mode 100644
index 00000000000..09c13aa8471
--- /dev/null
+++ b/lib/api/entities/job_request/job_info.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class JobInfo < Grape::Entity
+ expose :name, :stage
+ expose :project_id, :project_name
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/port.rb b/lib/api/entities/job_request/port.rb
new file mode 100644
index 00000000000..ee427da8657
--- /dev/null
+++ b/lib/api/entities/job_request/port.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Port < Grape::Entity
+ expose :number, :protocol, :name
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/response.rb b/lib/api/entities/job_request/response.rb
new file mode 100644
index 00000000000..fdacd3af2da
--- /dev/null
+++ b/lib/api/entities/job_request/response.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Response < Grape::Entity
+ expose :id
+ expose :token
+ expose :allow_git_fetch
+
+ expose :job_info, using: Entities::JobRequest::JobInfo do |model|
+ model
+ end
+
+ expose :git_info, using: Entities::JobRequest::GitInfo do |model|
+ model
+ end
+
+ expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model|
+ model
+ end
+
+ expose :variables
+ expose :steps, using: Entities::JobRequest::Step
+ expose :image, using: Entities::JobRequest::Image
+ expose :services, using: Entities::JobRequest::Service
+ expose :artifacts, using: Entities::JobRequest::Artifacts
+ expose :cache, using: Entities::JobRequest::Cache
+ expose :credentials, using: Entities::JobRequest::Credentials
+ expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency
+ expose :features
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/runner_info.rb b/lib/api/entities/job_request/runner_info.rb
new file mode 100644
index 00000000000..e6d2e8d9e85
--- /dev/null
+++ b/lib/api/entities/job_request/runner_info.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class RunnerInfo < Grape::Entity
+ expose :metadata_timeout, as: :timeout
+ expose :runner_session_url
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/service.rb b/lib/api/entities/job_request/service.rb
new file mode 100644
index 00000000000..9ad5abf4e9e
--- /dev/null
+++ b/lib/api/entities/job_request/service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Service < Entities::JobRequest::Image
+ expose :alias, :command
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/step.rb b/lib/api/entities/job_request/step.rb
new file mode 100644
index 00000000000..498dd017fb4
--- /dev/null
+++ b/lib/api/entities/job_request/step.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module JobRequest
+ class Step < Grape::Entity
+ expose :name, :script, :timeout, :when, :allow_failure
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/label.rb b/lib/api/entities/label.rb
new file mode 100644
index 00000000000..ca9a0912331
--- /dev/null
+++ b/lib/api/entities/label.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Label < Entities::LabelBasic
+ with_options if: lambda { |_, options| options[:with_counts] } do
+ expose :open_issues_count do |label, options|
+ label.open_issues_count(options[:current_user])
+ end
+
+ expose :closed_issues_count do |label, options|
+ label.closed_issues_count(options[:current_user])
+ end
+
+ expose :open_merge_requests_count do |label, options|
+ label.open_merge_requests_count(options[:current_user])
+ end
+ end
+
+ expose :subscribed do |label, options|
+ label.subscribed?(options[:current_user], options[:parent])
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/label_basic.rb b/lib/api/entities/label_basic.rb
new file mode 100644
index 00000000000..ed52688638e
--- /dev/null
+++ b/lib/api/entities/label_basic.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class LabelBasic < Grape::Entity
+ expose :id, :name, :color, :description, :description_html, :text_color
+ end
+ end
+end
diff --git a/lib/api/entities/license.rb b/lib/api/entities/license.rb
new file mode 100644
index 00000000000..d7a414344c1
--- /dev/null
+++ b/lib/api/entities/license.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class License < Entities::LicenseBasic
+ expose :popular?, as: :popular
+ expose(:description) { |license| license.meta['description'] }
+ expose(:conditions) { |license| license.meta['conditions'] }
+ expose(:permissions) { |license| license.meta['permissions'] }
+ expose(:limitations) { |license| license.meta['limitations'] }
+ expose :content
+ end
+ end
+end
diff --git a/lib/api/entities/license_basic.rb b/lib/api/entities/license_basic.rb
new file mode 100644
index 00000000000..08af68785a9
--- /dev/null
+++ b/lib/api/entities/license_basic.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class LicenseBasic < Grape::Entity
+ expose :key, :name, :nickname
+ expose :url, as: :html_url
+ expose(:source_url) { |license| license.meta['source'] }
+ end
+ end
+end
diff --git a/lib/api/entities/list.rb b/lib/api/entities/list.rb
new file mode 100644
index 00000000000..480e722c22c
--- /dev/null
+++ b/lib/api/entities/list.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class List < Grape::Entity
+ expose :id
+ expose :label, using: Entities::LabelBasic
+ expose :position
+ end
+ end
+end
+
+API::Entities::List.prepend_if_ee('EE::API::Entities::List')
diff --git a/lib/api/entities/member.rb b/lib/api/entities/member.rb
new file mode 100644
index 00000000000..14e97f41e77
--- /dev/null
+++ b/lib/api/entities/member.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Member < Grape::Entity
+ expose :user, merge: true, using: UserBasic
+ expose :access_level
+ expose :expires_at
+ end
+ end
+end
+
+API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true)
diff --git a/lib/api/entities/member_access.rb b/lib/api/entities/member_access.rb
new file mode 100644
index 00000000000..097c72bf617
--- /dev/null
+++ b/lib/api/entities/member_access.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MemberAccess < Grape::Entity
+ expose :access_level
+ expose :notification_level do |member, options|
+ if member.notification_setting
+ ::NotificationSetting.levels[member.notification_setting.level]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/membership.rb b/lib/api/entities/membership.rb
new file mode 100644
index 00000000000..2e3e6a0d8ba
--- /dev/null
+++ b/lib/api/entities/membership.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Membership < Grape::Entity
+ expose :source_id
+ expose :source_name do |member|
+ member.source.name
+ end
+ expose :source_type
+ expose :access_level
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb
new file mode 100644
index 00000000000..9ff8e20ced1
--- /dev/null
+++ b/lib/api/entities/merge_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequest < MergeRequestBasic
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options|
+ merge_request.subscribed?(options[:current_user], options[:project])
+ end
+
+ expose :changes_count do |merge_request, _options|
+ merge_request.merge_request_diff.real_size
+ end
+
+ expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_started_at
+ end
+
+ expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_finished_at
+ end
+
+ expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.first_deployed_to_production_at
+ end
+
+ expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.pipeline
+ end
+
+ expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
+ Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
+ end
+
+ expose :diff_refs, using: Entities::DiffRefs
+
+ # Allow the status of a rebase to be determined
+ expose :merge_error
+ expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] }
+
+ expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] }
+
+ def build_available?(options)
+ options[:project]&.feature_available?(:builds, options[:current_user])
+ end
+
+ expose :user do
+ expose :can_merge do |merge_request, options|
+ merge_request.can_be_merged_by?(options[:current_user])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
new file mode 100644
index 00000000000..8cec2c1a97e
--- /dev/null
+++ b/lib/api/entities/merge_request_basic.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestBasic < IssuableEntity
+ expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.merged_by
+ end
+
+ expose :merged_at do |merge_request, _options|
+ merge_request.metrics&.merged_at
+ end
+
+ expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.latest_closed_by
+ end
+
+ expose :closed_at do |merge_request, _options|
+ merge_request.metrics&.latest_closed_at
+ end
+
+ expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :title)
+ end
+ expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :description)
+ end
+ expose :target_branch, :source_branch
+ expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
+ expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
+ expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
+ expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
+ merge_request.assignee
+ end
+ expose :author, :assignees, using: Entities::UserBasic
+
+ expose :source_project_id, :target_project_id
+ expose :labels do |merge_request, options|
+ if options[:with_labels_details]
+ ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title))
+ else
+ merge_request.labels.map(&:title).sort
+ end
+ end
+ expose :work_in_progress?, as: :work_in_progress
+ expose :milestone, using: Entities::Milestone
+ expose :merge_when_pipeline_succeeds
+
+ # Ideally we should deprecate `MergeRequest#merge_status` exposure and
+ # use `MergeRequest#mergeable?` instead (boolean).
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
+ # information.
+ expose :merge_status do |merge_request|
+ merge_request.check_mergeability(async: true)
+ merge_request.merge_status
+ end
+ expose :diff_head_sha, as: :sha
+ expose :merge_commit_sha
+ expose :squash_commit_sha
+ expose :discussion_locked
+ expose :should_remove_source_branch?, as: :should_remove_source_branch
+ expose :force_remove_source_branch?, as: :force_remove_source_branch
+ expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
+ # Deprecated
+ expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
+
+ # reference is deprecated in favour of references
+ # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354)
+ expose :reference do |merge_request, options|
+ merge_request.to_reference(options[:project])
+ end
+
+ expose :references, with: IssuableReferences do |merge_request|
+ merge_request
+ end
+
+ expose :web_url do |merge_request|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
+ merge_request
+ end
+
+ expose :squash
+
+ expose :task_completion_status
+
+ expose :cannot_be_merged?, as: :has_conflicts
+
+ expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
+ end
+ end
+end
+
+API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true)
diff --git a/lib/api/entities/merge_request_changes.rb b/lib/api/entities/merge_request_changes.rb
new file mode 100644
index 00000000000..a835d119736
--- /dev/null
+++ b/lib/api/entities/merge_request_changes.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestChanges < MergeRequest
+ expose :diffs, as: :changes, using: Entities::Diff do |compare, _|
+ compare.raw_diffs(limits: false).to_a
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_diff.rb b/lib/api/entities/merge_request_diff.rb
new file mode 100644
index 00000000000..3eda1400855
--- /dev/null
+++ b/lib/api/entities/merge_request_diff.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestDiff < Grape::Entity
+ expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+ :created_at, :merge_request_id, :state, :real_size
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_diff_full.rb b/lib/api/entities/merge_request_diff_full.rb
new file mode 100644
index 00000000000..772b9b6822c
--- /dev/null
+++ b/lib/api/entities/merge_request_diff_full.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestDiffFull < MergeRequestDiff
+ expose :commits, using: Entities::Commit
+
+ expose :diffs, using: Entities::Diff do |compare, _|
+ compare.raw_diffs(limits: false).to_a
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_simple.rb b/lib/api/entities/merge_request_simple.rb
new file mode 100644
index 00000000000..f3ff4cc18a8
--- /dev/null
+++ b/lib/api/entities/merge_request_simple.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestSimple < IssuableEntity
+ expose :title
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb
new file mode 100644
index 00000000000..5a0c222d691
--- /dev/null
+++ b/lib/api/entities/milestone.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Milestone < Grape::Entity
+ expose :id, :iid
+ expose :project_id, if: -> (entity, options) { entity&.project_id }
+ expose :group_id, if: -> (entity, options) { entity&.group_id }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+ expose :due_date
+ expose :start_date
+
+ expose :web_url do |milestone, _options|
+ Gitlab::UrlBuilder.build(milestone)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/mr_note.rb b/lib/api/entities/mr_note.rb
new file mode 100644
index 00000000000..283f7bd1092
--- /dev/null
+++ b/lib/api/entities/mr_note.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MRNote < Grape::Entity
+ expose :note
+ expose :author, using: Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/namespace.rb b/lib/api/entities/namespace.rb
new file mode 100644
index 00000000000..a7e06cc3e02
--- /dev/null
+++ b/lib/api/entities/namespace.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Namespace < Entities::NamespaceBasic
+ expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _|
+ namespace.users_with_descendants.count
+ end
+
+ def expose_members_count_with_descendants?(namespace, opts)
+ namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
+ end
+ end
+ end
+end
+
+API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace')
diff --git a/lib/api/entities/namespace_basic.rb b/lib/api/entities/namespace_basic.rb
new file mode 100644
index 00000000000..f968a074bd2
--- /dev/null
+++ b/lib/api/entities/namespace_basic.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class NamespaceBasic < Grape::Entity
+ expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url
+
+ expose :web_url do |namespace|
+ if namespace.user?
+ Gitlab::Routing.url_helpers.user_url(namespace.owner)
+ else
+ namespace.web_url
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb
new file mode 100644
index 00000000000..dcfb9a6d670
--- /dev/null
+++ b/lib/api/entities/note.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Note < Grape::Entity
+ # Only Issue and MergeRequest have iid
+ NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
+
+ expose :id
+ expose :type
+ expose :note, as: :body
+ expose :attachment_identifier, as: :attachment
+ expose :author, using: Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :system?, as: :system
+ expose :noteable_id, :noteable_type
+
+ expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note|
+ note.position.to_h
+ end
+
+ expose :resolvable?, as: :resolvable
+ expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? }
+ expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? }
+
+ # Avoid N+1 queries as much as possible
+ expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
+ end
+ end
+end
diff --git a/lib/api/entities/notification_setting.rb b/lib/api/entities/notification_setting.rb
new file mode 100644
index 00000000000..cdff4f2f5c5
--- /dev/null
+++ b/lib/api/entities/notification_setting.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class NotificationSetting < Grape::Entity
+ expose :level
+ expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
+ ::NotificationSetting.email_events.each do |event|
+ expose event
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pages_domain.rb b/lib/api/entities/pages_domain.rb
new file mode 100644
index 00000000000..87af8c7b0a4
--- /dev/null
+++ b/lib/api/entities/pages_domain.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PagesDomain < Grape::Entity
+ expose :domain
+ expose :url
+ expose :verified?, as: :verified
+ expose :verification_code, as: :verification_code
+ expose :enabled_until
+ expose :auto_ssl_enabled
+
+ expose :certificate,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: Entities::PagesDomainCertificate do |pages_domain|
+ pages_domain
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pages_domain_basic.rb b/lib/api/entities/pages_domain_basic.rb
new file mode 100644
index 00000000000..6f8901fe715
--- /dev/null
+++ b/lib/api/entities/pages_domain_basic.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PagesDomainBasic < Grape::Entity
+ expose :domain
+ expose :url
+ expose :project_id
+ expose :verified?, as: :verified
+ expose :verification_code, as: :verification_code
+ expose :enabled_until
+ expose :auto_ssl_enabled
+
+ expose :certificate,
+ as: :certificate_expiration,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: Entities::PagesDomainCertificateExpiration do |pages_domain|
+ pages_domain
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pages_domain_certificate.rb b/lib/api/entities/pages_domain_certificate.rb
new file mode 100644
index 00000000000..82c4729d454
--- /dev/null
+++ b/lib/api/entities/pages_domain_certificate.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PagesDomainCertificate < Grape::Entity
+ expose :subject
+ expose :expired?, as: :expired
+ expose :certificate
+ expose :certificate_text
+ end
+ end
+end
diff --git a/lib/api/entities/pages_domain_certificate_expiration.rb b/lib/api/entities/pages_domain_certificate_expiration.rb
new file mode 100644
index 00000000000..bfc70f6657f
--- /dev/null
+++ b/lib/api/entities/pages_domain_certificate_expiration.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PagesDomainCertificateExpiration < Grape::Entity
+ expose :expired?, as: :expired
+ expose :expiration
+ end
+ end
+end
diff --git a/lib/api/entities/personal_access_token.rb b/lib/api/entities/personal_access_token.rb
new file mode 100644
index 00000000000..d6fb9af6ab3
--- /dev/null
+++ b/lib/api/entities/personal_access_token.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PersonalAccessToken < Grape::Entity
+ expose :id, :name, :revoked, :created_at, :scopes
+ expose :active?, as: :active
+ expose :expires_at do |personal_access_token|
+ personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/personal_access_token_with_token.rb b/lib/api/entities/personal_access_token_with_token.rb
new file mode 100644
index 00000000000..84dcd3bc8d8
--- /dev/null
+++ b/lib/api/entities/personal_access_token_with_token.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PersonalAccessTokenWithToken < Entities::PersonalAccessToken
+ expose :token
+ end
+ end
+end
diff --git a/lib/api/entities/personal_snippet.rb b/lib/api/entities/personal_snippet.rb
new file mode 100644
index 00000000000..eb0266e61e6
--- /dev/null
+++ b/lib/api/entities/personal_snippet.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PersonalSnippet < Snippet
+ expose :raw_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet, raw: true)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pipeline.rb b/lib/api/entities/pipeline.rb
new file mode 100644
index 00000000000..778efbe4bcc
--- /dev/null
+++ b/lib/api/entities/pipeline.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Pipeline < Entities::PipelineBasic
+ expose :before_sha, :tag, :yaml_errors
+
+ expose :user, with: Entities::UserBasic
+ expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+ expose :duration
+ expose :coverage
+ expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
+ pipeline.detailed_status(options[:current_user])
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pipeline_basic.rb b/lib/api/entities/pipeline_basic.rb
new file mode 100644
index 00000000000..359f6a447ab
--- /dev/null
+++ b/lib/api/entities/pipeline_basic.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ expose :created_at, :updated_at
+
+ expose :web_url do |pipeline, _options|
+ Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pipeline_schedule.rb b/lib/api/entities/pipeline_schedule.rb
new file mode 100644
index 00000000000..a72fe3f3141
--- /dev/null
+++ b/lib/api/entities/pipeline_schedule.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PipelineSchedule < Grape::Entity
+ expose :id
+ expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
+ expose :created_at, :updated_at
+ expose :owner, using: Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/pipeline_schedule_details.rb b/lib/api/entities/pipeline_schedule_details.rb
new file mode 100644
index 00000000000..5e54489a0f9
--- /dev/null
+++ b/lib/api/entities/pipeline_schedule_details.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PipelineScheduleDetails < Entities::PipelineSchedule
+ expose :last_pipeline, using: Entities::PipelineBasic
+ expose :variables, using: Entities::Variable
+ end
+ end
+end
diff --git a/lib/api/entities/platform/kubernetes.rb b/lib/api/entities/platform/kubernetes.rb
new file mode 100644
index 00000000000..eeb6d57bb8f
--- /dev/null
+++ b/lib/api/entities/platform/kubernetes.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Platform
+ class Kubernetes < Grape::Entity
+ expose :api_url
+ expose :namespace
+ expose :authorization_type
+ expose :ca_cert
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
new file mode 100644
index 00000000000..6ed2ed34360
--- /dev/null
+++ b/lib/api/entities/project.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Project < BasicProjectDetails
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :_links do
+ expose :self do |project|
+ expose_url(api_v4_projects_path(id: project.id))
+ end
+
+ expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project|
+ expose_url(api_v4_projects_issues_path(id: project.id))
+ end
+
+ expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project|
+ expose_url(api_v4_projects_merge_requests_path(id: project.id))
+ end
+
+ expose :repo_branches do |project|
+ expose_url(api_v4_projects_repository_branches_path(id: project.id))
+ end
+
+ expose :labels do |project|
+ expose_url(api_v4_projects_labels_path(id: project.id))
+ end
+
+ expose :events do |project|
+ expose_url(api_v4_projects_events_path(id: project.id))
+ end
+
+ expose :members do |project|
+ expose_url(api_v4_projects_members_path(id: project.id))
+ end
+ end
+
+ expose :empty_repo?, as: :empty_repo
+ expose :archived?, as: :archived
+ expose :visibility
+ expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
+ expose :resolve_outdated_diff_discussions
+ expose :container_registry_enabled
+ expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy,
+ if: -> (project, _) { project.container_expiration_policy }
+
+ # Expose old field names with the new permissions methods to keep API compatible
+ # TODO: remove in API v5, replaced by *_access_level
+ expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
+ expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
+ expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+
+ expose(:can_create_merge_request_in) do |project, options|
+ Ability.allowed?(options[:current_user], :create_merge_request_in, project)
+ end
+
+ expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) }
+ expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) }
+ expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) }
+ expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) }
+ expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
+ expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
+ expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) }
+
+ expose :emails_disabled
+ expose :shared_runners_enabled
+ expose :lfs_enabled?, as: :lfs_enabled
+ expose :creator_id
+ expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do
+ project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project)
+ end
+ expose :import_status
+
+ expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project|
+ project.import_state&.last_error
+ end
+
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
+ expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
+ expose :ci_default_git_depth
+ expose :public_builds, as: :public_jobs
+ expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options|
+ project.build_allow_git_fetch ? 'fetch' : 'clone'
+ end
+ expose :build_timeout
+ expose :auto_cancel_pending_pipelines
+ expose :build_coverage_regex
+ expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ expose :shared_with_groups do |project, options|
+ SharedGroup.represent(project.project_group_links, options)
+ end
+ expose :only_allow_merge_if_pipeline_succeeds
+ expose :request_access_enabled
+ expose :only_allow_merge_if_all_discussions_are_resolved
+ expose :remove_source_branch_after_merge
+ expose :printing_merge_request_link_enabled
+ expose :merge_method
+ expose :suggestion_commit_message
+ expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
+ options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
+ }
+ expose :auto_devops_enabled?, as: :auto_devops_enabled
+ expose :auto_devops_deploy_strategy do |project, options|
+ project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
+ end
+ expose :autoclose_referenced_issues
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.preload_relation(projects_relation, options = {})
+ # Preloading tags, should be done with using only `:tags`,
+ # as `:tags` are defined as: `has_many :tags, through: :taggings`
+ # N+1 is solved then by using `subject.tags.map(&:name)`
+ # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555
+ super(projects_relation).preload(:group)
+ .preload(:ci_cd_settings)
+ .preload(:container_expiration_policy)
+ .preload(:auto_devops)
+ .preload(project_group_links: { group: :route },
+ fork_network: :root_project,
+ fork_network_member: :forked_from_project,
+ forked_from_project: [:route, :forks, :tags, namespace: :route])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def self.forks_counting_projects(projects_relation)
+ projects_relation + projects_relation.map(&:forked_from_project).compact
+ end
+ end
+ end
+end
+
+API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true)
diff --git a/lib/api/entities/project_access.rb b/lib/api/entities/project_access.rb
new file mode 100644
index 00000000000..29f85fda620
--- /dev/null
+++ b/lib/api/entities/project_access.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectAccess < Entities::MemberAccess
+ end
+ end
+end
diff --git a/lib/api/entities/project_daily_fetches.rb b/lib/api/entities/project_daily_fetches.rb
new file mode 100644
index 00000000000..036b5dc99b8
--- /dev/null
+++ b/lib/api/entities/project_daily_fetches.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectDailyFetches < Grape::Entity
+ expose :fetch_count, as: :count
+ expose :date
+ end
+ end
+end
diff --git a/lib/api/entities/project_daily_statistics.rb b/lib/api/entities/project_daily_statistics.rb
new file mode 100644
index 00000000000..803ee445851
--- /dev/null
+++ b/lib/api/entities/project_daily_statistics.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectDailyStatistics < Grape::Entity
+ expose :fetches do
+ expose :total_fetch_count, as: :total
+ expose :fetches, as: :days, using: ProjectDailyFetches
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project_export_status.rb b/lib/api/entities/project_export_status.rb
new file mode 100644
index 00000000000..ad84a45996a
--- /dev/null
+++ b/lib/api/entities/project_export_status.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectExportStatus < ProjectIdentity
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :export_status
+ expose :_links, if: lambda { |project, _options| project.export_status == :finished } do
+ expose :api_url do |project|
+ expose_url(api_v4_projects_export_download_path(id: project.id))
+ end
+
+ expose :web_url do |project|
+ Gitlab::Routing.url_helpers.download_export_project_url(project)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project_group_link.rb b/lib/api/entities/project_group_link.rb
new file mode 100644
index 00000000000..89138854e67
--- /dev/null
+++ b/lib/api/entities/project_group_link.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectGroupLink < Grape::Entity
+ expose :id, :project_id, :group_id, :group_access, :expires_at
+ end
+ end
+end
diff --git a/lib/api/entities/project_hook.rb b/lib/api/entities/project_hook.rb
new file mode 100644
index 00000000000..cdd3714ed64
--- /dev/null
+++ b/lib/api/entities/project_hook.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectHook < Hook
+ expose :project_id, :issues_events, :confidential_issues_events
+ expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events
+ expose :job_events
+ expose :push_events_branch_filter
+ end
+ end
+end
diff --git a/lib/api/entities/project_identity.rb b/lib/api/entities/project_identity.rb
new file mode 100644
index 00000000000..2055195eea0
--- /dev/null
+++ b/lib/api/entities/project_identity.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectIdentity < Grape::Entity
+ expose :id, :description
+ expose :name, :name_with_namespace
+ expose :path, :path_with_namespace
+ expose :created_at
+ end
+ end
+end
diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb
new file mode 100644
index 00000000000..0b884b43e9e
--- /dev/null
+++ b/lib/api/entities/project_import_status.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectImportStatus < ProjectIdentity
+ expose :import_status
+
+ # TODO: Use `expose_nil` once we upgrade the grape-entity gem
+ expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project|
+ project.import_state.last_error
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project_label.rb b/lib/api/entities/project_label.rb
new file mode 100644
index 00000000000..b47a9414ddb
--- /dev/null
+++ b/lib/api/entities/project_label.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectLabel < Entities::Label
+ expose :priority do |label, options|
+ label.priority(options[:parent])
+ end
+ expose :is_project_label do |label, options|
+ label.is_a?(::ProjectLabel)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project_service.rb b/lib/api/entities/project_service.rb
new file mode 100644
index 00000000000..947cec1e3cd
--- /dev/null
+++ b/lib/api/entities/project_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectService < Entities::ProjectServiceBasic
+ # Expose serialized properties
+ expose :properties do |service, options|
+ # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
+ if service.data_fields_present?
+ service.data_fields.as_json.slice(*service.api_field_names)
+ else
+ service.properties.slice(*service.api_field_names)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/project_service_basic.rb b/lib/api/entities/project_service_basic.rb
new file mode 100644
index 00000000000..eb97ca69a82
--- /dev/null
+++ b/lib/api/entities/project_service_basic.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectServiceBasic < Grape::Entity
+ expose :id, :title
+ expose :slug do |service|
+ service.to_param.dasherize
+ end
+ expose :created_at, :updated_at, :active
+ expose :commit_events, :push_events, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :tag_push_events, :note_events
+ expose :confidential_note_events, :pipeline_events, :wiki_page_events
+ expose :job_events, :comment_on_event_enabled
+ end
+ end
+end
diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb
new file mode 100644
index 00000000000..8ed87e51375
--- /dev/null
+++ b/lib/api/entities/project_snippet.rb
@@ -0,0 +1,8 @@
+# frozen_String_literal: true
+
+module API
+ module Entities
+ class ProjectSnippet < Entities::Snippet
+ end
+ end
+end
diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb
new file mode 100644
index 00000000000..e5f6165da31
--- /dev/null
+++ b/lib/api/entities/project_statistics.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectStatistics < Grape::Entity
+ expose :commit_count
+ expose :storage_size
+ expose :repository_size
+ expose :wiki_size
+ expose :lfs_objects_size
+ expose :build_artifacts_size, as: :job_artifacts_size
+ end
+ end
+end
diff --git a/lib/api/entities/project_with_access.rb b/lib/api/entities/project_with_access.rb
new file mode 100644
index 00000000000..c53a712a879
--- /dev/null
+++ b/lib/api/entities/project_with_access.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectWithAccess < Project
+ expose :permissions do
+ expose :project_access, using: Entities::ProjectAccess do |project, options|
+ if options[:project_members]
+ options[:project_members].find { |member| member.source_id == project.id }
+ else
+ project.project_member(options[:current_user])
+ end
+ end
+
+ expose :group_access, using: Entities::GroupAccess do |project, options|
+ if project.group
+ if options[:group_members]
+ options[:group_members].find { |member| member.source_id == project.namespace_id }
+ else
+ project.group.highest_group_member(options[:current_user])
+ end
+ end
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.preload_relation(projects_relation, options = {})
+ relation = super(projects_relation, options)
+ project_ids = relation.select('projects.id')
+ namespace_ids = relation.select(:namespace_id)
+
+ options[:project_members] = options[:current_user]
+ .project_members
+ .where(source_id: project_ids)
+ .preload(:source, user: [notification_settings: :source])
+
+ options[:group_members] = options[:current_user]
+ .group_members
+ .where(source_id: namespace_ids)
+ .preload(:source, user: [notification_settings: :source])
+
+ relation
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/api/entities/protected_branch.rb b/lib/api/entities/protected_branch.rb
new file mode 100644
index 00000000000..80c8a791053
--- /dev/null
+++ b/lib/api/entities/protected_branch.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProtectedBranch < Grape::Entity
+ expose :id
+ expose :name
+ expose :push_access_levels, using: Entities::ProtectedRefAccess
+ expose :merge_access_levels, using: Entities::ProtectedRefAccess
+ end
+ end
+end
+
+API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch')
diff --git a/lib/api/entities/protected_ref_access.rb b/lib/api/entities/protected_ref_access.rb
new file mode 100644
index 00000000000..f0185705b06
--- /dev/null
+++ b/lib/api/entities/protected_ref_access.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProtectedRefAccess < Grape::Entity
+ expose :access_level
+ expose :access_level_description do |protected_ref_access|
+ protected_ref_access.humanize
+ end
+ end
+ end
+end
+
+API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess')
diff --git a/lib/api/entities/protected_tag.rb b/lib/api/entities/protected_tag.rb
new file mode 100644
index 00000000000..dc397f01af6
--- /dev/null
+++ b/lib/api/entities/protected_tag.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProtectedTag < Grape::Entity
+ expose :name
+ expose :create_access_levels, using: Entities::ProtectedRefAccess
+ end
+ end
+end
diff --git a/lib/api/entities/provider/gcp.rb b/lib/api/entities/provider/gcp.rb
new file mode 100644
index 00000000000..85f56a9ac1e
--- /dev/null
+++ b/lib/api/entities/provider/gcp.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Provider
+ class Gcp < Grape::Entity
+ expose :cluster_id
+ expose :status_name
+ expose :gcp_project_id
+ expose :zone
+ expose :machine_type
+ expose :num_nodes
+ expose :endpoint
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/push_event_payload.rb b/lib/api/entities/push_event_payload.rb
new file mode 100644
index 00000000000..6aad5f10177
--- /dev/null
+++ b/lib/api/entities/push_event_payload.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PushEventPayload < Grape::Entity
+ expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref,
+ :commit_title, :ref_count
+ end
+ end
+end
diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb
new file mode 100644
index 00000000000..dc4b91e594e
--- /dev/null
+++ b/lib/api/entities/release.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Release < Grape::Entity
+ include ::API::Helpers::Presentable
+
+ expose :name do |release, _|
+ can_download_code? ? release.name : "Release-#{release.id}"
+ end
+ expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
+ expose :description
+ expose :description_html do |entity|
+ MarkupHelper.markdown_field(entity, :description)
+ end
+ expose :created_at
+ expose :released_at
+ expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
+ expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
+ expose :upcoming_release?, as: :upcoming_release
+ expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? && can_read_milestone? }
+ expose :commit_path, expose_nil: false
+ expose :tag_path, expose_nil: false
+ expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
+ expose :assets do
+ expose :assets_count, as: :count do |release, _|
+ assets_to_exclude = can_download_code? ? [] : [:sources]
+ release.assets_count(except: assets_to_exclude)
+ end
+ expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
+ expose :links, using: Entities::Releases::Link do |release, options|
+ release.links.sorted
+ end
+ expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
+ end
+ expose :_links do
+ expose :self_url, as: :self, expose_nil: false
+ expose :merge_requests_url, expose_nil: false
+ expose :issues_url, expose_nil: false
+ expose :edit_url, expose_nil: false
+ end
+
+ private
+
+ def can_download_code?
+ Ability.allowed?(options[:current_user], :download_code, object.project)
+ end
+
+ def can_read_milestone?
+ Ability.allowed?(options[:current_user], :read_milestone, object.project)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb
new file mode 100644
index 00000000000..6cc01e0e981
--- /dev/null
+++ b/lib/api/entities/releases/link.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Releases
+ class Link < Grape::Entity
+ expose :id
+ expose :name
+ expose :url
+ expose :external?, as: :external
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/releases/source.rb b/lib/api/entities/releases/source.rb
new file mode 100644
index 00000000000..2b0c8038ddf
--- /dev/null
+++ b/lib/api/entities/releases/source.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Releases
+ class Source < Grape::Entity
+ expose :format
+ expose :url
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/remote_mirror.rb b/lib/api/entities/remote_mirror.rb
new file mode 100644
index 00000000000..dde3e9dea99
--- /dev/null
+++ b/lib/api/entities/remote_mirror.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class RemoteMirror < Grape::Entity
+ expose :id
+ expose :enabled
+ expose :safe_url, as: :url
+ expose :update_status
+ expose :last_update_at
+ expose :last_update_started_at
+ expose :last_successful_update_at
+ expose :last_error
+ expose :only_protected_branches
+ end
+ end
+end
diff --git a/lib/api/entities/resource_label_event.rb b/lib/api/entities/resource_label_event.rb
new file mode 100644
index 00000000000..890264abf93
--- /dev/null
+++ b/lib/api/entities/resource_label_event.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ResourceLabelEvent < Grape::Entity
+ expose :id
+ expose :user, using: Entities::UserBasic
+ expose :created_at
+ expose :resource_type do |event, options|
+ event.issuable.class.name
+ end
+ expose :resource_id do |event, options|
+ event.issuable.id
+ end
+ expose :label, using: Entities::LabelBasic
+ expose :action
+ end
+ end
+end
diff --git a/lib/api/entities/runner.rb b/lib/api/entities/runner.rb
new file mode 100644
index 00000000000..6165b54cddb
--- /dev/null
+++ b/lib/api/entities/runner.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Runner < Grape::Entity
+ expose :id
+ expose :description
+ expose :ip_address
+ expose :active
+ expose :instance_type?, as: :is_shared
+ expose :name
+ expose :online?, as: :online
+ expose :status
+ end
+ end
+end
diff --git a/lib/api/entities/runner_details.rb b/lib/api/entities/runner_details.rb
new file mode 100644
index 00000000000..17202821e6e
--- /dev/null
+++ b/lib/api/entities/runner_details.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class RunnerDetails < Runner
+ expose :tag_list
+ expose :run_untagged
+ expose :locked
+ expose :maximum_timeout
+ expose :access_level
+ expose :version, :revision, :platform, :architecture
+ expose :contacted_at
+ expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
+ # rubocop: disable CodeReuse/ActiveRecord
+ expose :projects, with: Entities::BasicProjectDetails do |runner, options|
+ if options[:current_user].admin?
+ runner.projects
+ else
+ options[:current_user].authorized_projects.where(id: runner.projects)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ expose :groups, with: Entities::BasicGroupDetails do |runner, options|
+ if options[:current_user].admin?
+ runner.groups
+ else
+ options[:current_user].authorized_groups.where(id: runner.groups)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/api/entities/runner_registration_details.rb b/lib/api/entities/runner_registration_details.rb
new file mode 100644
index 00000000000..c8ed88ba10a
--- /dev/null
+++ b/lib/api/entities/runner_registration_details.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class RunnerRegistrationDetails < Grape::Entity
+ expose :id, :token
+ end
+ end
+end
diff --git a/lib/api/entities/shared_group.rb b/lib/api/entities/shared_group.rb
new file mode 100644
index 00000000000..862e73e07f0
--- /dev/null
+++ b/lib/api/entities/shared_group.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class SharedGroup < Grape::Entity
+ expose :group_id
+ expose :group_name do |group_link, options|
+ group_link.group.name
+ end
+ expose :group_full_path do |group_link, options|
+ group_link.group.full_path
+ end
+ expose :group_access, as: :group_access_level
+ expose :expires_at
+ end
+ end
+end
diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb
new file mode 100644
index 00000000000..d92f7b79c28
--- /dev/null
+++ b/lib/api/entities/snippet.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Snippet < Grape::Entity
+ expose :id, :title, :file_name, :description, :visibility
+ expose :author, using: Entities::UserBasic
+ expose :updated_at, :created_at
+ expose :project_id
+ expose :web_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ssh_key.rb b/lib/api/entities/ssh_key.rb
new file mode 100644
index 00000000000..0e2f6ebae8c
--- /dev/null
+++ b/lib/api/entities/ssh_key.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class SSHKey < Grape::Entity
+ expose :id, :title, :key, :created_at
+ end
+ end
+end
diff --git a/lib/api/entities/ssh_key_with_user.rb b/lib/api/entities/ssh_key_with_user.rb
new file mode 100644
index 00000000000..95559bbf2ac
--- /dev/null
+++ b/lib/api/entities/ssh_key_with_user.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class SSHKeyWithUser < Entities::SSHKey
+ expose :user, using: Entities::UserPublic
+ end
+ end
+end
diff --git a/lib/api/entities/suggestion.rb b/lib/api/entities/suggestion.rb
new file mode 100644
index 00000000000..59f94099d7f
--- /dev/null
+++ b/lib/api/entities/suggestion.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Suggestion < Grape::Entity
+ expose :id
+ expose :from_line
+ expose :to_line
+ expose :appliable?, as: :appliable
+ expose :applied
+ expose :from_content
+ expose :to_content
+ end
+ end
+end
diff --git a/lib/api/entities/tag.rb b/lib/api/entities/tag.rb
new file mode 100644
index 00000000000..2d3569bb9bb
--- /dev/null
+++ b/lib/api/entities/tag.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Tag < Grape::Entity
+ expose :name, :message, :target
+
+ expose :commit, using: Entities::Commit do |repo_tag, options|
+ options[:project].repository.commit(repo_tag.dereferenced_target)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ expose :release, using: Entities::TagRelease do |repo_tag, options|
+ options[:project].releases.find_by(tag: repo_tag.name)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ expose :protected do |repo_tag, options|
+ ::ProtectedTag.protected?(options[:project], repo_tag.name)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/tag_release.rb b/lib/api/entities/tag_release.rb
new file mode 100644
index 00000000000..d5f73d60332
--- /dev/null
+++ b/lib/api/entities/tag_release.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ # deprecated old Release representation
+ class TagRelease < Grape::Entity
+ expose :tag, as: :tag_name
+ expose :description
+ end
+ end
+end
diff --git a/lib/api/entities/template.rb b/lib/api/entities/template.rb
new file mode 100644
index 00000000000..ef364d971bf
--- /dev/null
+++ b/lib/api/entities/template.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Template < Grape::Entity
+ expose :name, :content
+ end
+ end
+end
diff --git a/lib/api/entities/templates_list.rb b/lib/api/entities/templates_list.rb
new file mode 100644
index 00000000000..8e8aa1bd285
--- /dev/null
+++ b/lib/api/entities/templates_list.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class TemplatesList < Grape::Entity
+ expose :key
+ expose :name
+ end
+ end
+end
diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb
new file mode 100644
index 00000000000..abfdde89bf1
--- /dev/null
+++ b/lib/api/entities/todo.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Todo < Grape::Entity
+ expose :id
+ expose :project, using: Entities::ProjectIdentity, if: -> (todo, _) { todo.project_id }
+ expose :group, using: 'API::Entities::NamespaceBasic', if: -> (todo, _) { todo.group_id }
+ expose :author, using: Entities::UserBasic
+ expose :action_name
+ expose :target_type
+
+ expose :target do |todo, options|
+ todo_options = options.fetch(todo.target_type, {})
+ todo_target_class(todo.target_type).represent(todo.target, todo_options)
+ end
+
+ expose :target_url do |todo, options|
+ todo_target_url(todo)
+ end
+
+ expose :body
+ expose :state
+ expose :created_at
+
+ def todo_target_class(target_type)
+ # false as second argument prevents looking up in module hierarchy
+ # see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719
+ ::API::Entities.const_get(target_type, false)
+ end
+
+ def todo_target_url(todo)
+ target_type = todo.target_type.underscore
+ target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
+
+ Gitlab::Routing
+ .url_helpers
+ .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def todo_target_anchor(todo)
+ "note_#{todo.note_id}" if todo.note_id?
+ end
+ end
+ end
+end
+
+API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo')
diff --git a/lib/api/entities/tree_object.rb b/lib/api/entities/tree_object.rb
new file mode 100644
index 00000000000..e4e840ebe43
--- /dev/null
+++ b/lib/api/entities/tree_object.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class TreeObject < Grape::Entity
+ expose :id, :name, :type, :path
+
+ expose :mode do |obj, options|
+ filemode = obj.mode
+ filemode = "0" + filemode if filemode.length < 6
+ filemode
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/trigger.rb b/lib/api/entities/trigger.rb
new file mode 100644
index 00000000000..6a9f772fc6b
--- /dev/null
+++ b/lib/api/entities/trigger.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Trigger < Grape::Entity
+ include ::API::Helpers::Presentable
+
+ expose :id
+ expose :token
+ expose :description
+ expose :created_at, :updated_at, :last_used
+ expose :owner, using: Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb
new file mode 100644
index 00000000000..15e4619cdb8
--- /dev/null
+++ b/lib/api/entities/user.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class User < UserBasic
+ expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
+ expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization
+ end
+ end
+end
diff --git a/lib/api/entities/user_activity.rb b/lib/api/entities/user_activity.rb
new file mode 100644
index 00000000000..30c23cc7a67
--- /dev/null
+++ b/lib/api/entities/user_activity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserActivity < Grape::Entity
+ expose :username
+ expose :last_activity_on
+ expose :last_activity_on, as: :last_activity_at # Back-compat
+ end
+ end
+end
diff --git a/lib/api/entities/user_agent_detail.rb b/lib/api/entities/user_agent_detail.rb
new file mode 100644
index 00000000000..a2d02c16589
--- /dev/null
+++ b/lib/api/entities/user_agent_detail.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserAgentDetail < Grape::Entity
+ expose :user_agent
+ expose :ip_address
+ expose :submitted, as: :akismet_submitted
+ end
+ end
+end
diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb
new file mode 100644
index 00000000000..e063aa42855
--- /dev/null
+++ b/lib/api/entities/user_basic.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserBasic < UserSafe
+ expose :state
+
+ expose :avatar_url do |user, options|
+ user.avatar_url(only_path: false)
+ end
+
+ expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path }
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
+
+ expose :web_url do |user, options|
+ Gitlab::Routing.url_helpers.user_url(user)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/user_details_with_admin.rb b/lib/api/entities/user_details_with_admin.rb
new file mode 100644
index 00000000000..9ea5c583437
--- /dev/null
+++ b/lib/api/entities/user_details_with_admin.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserDetailsWithAdmin < UserWithAdmin
+ expose :highest_role
+ expose :current_sign_in_ip
+ expose :last_sign_in_ip
+ end
+ end
+end
diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb
new file mode 100644
index 00000000000..15e9b905bef
--- /dev/null
+++ b/lib/api/entities/user_public.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserPublic < Entities::User
+ expose :last_sign_in_at
+ expose :confirmed_at
+ expose :last_activity_on
+ expose :email
+ expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :identities, using: Entities::Identity
+ expose :can_create_group?, as: :can_create_group
+ expose :can_create_project?, as: :can_create_project
+ expose :two_factor_enabled?, as: :two_factor_enabled
+ expose :external
+ expose :private_profile
+ end
+ end
+end
+
+API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true)
diff --git a/lib/api/entities/user_safe.rb b/lib/api/entities/user_safe.rb
new file mode 100644
index 00000000000..feb01767fd6
--- /dev/null
+++ b/lib/api/entities/user_safe.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserSafe < Grape::Entity
+ expose :id, :name, :username
+ end
+ end
+end
diff --git a/lib/api/entities/user_stars_project.rb b/lib/api/entities/user_stars_project.rb
new file mode 100644
index 00000000000..3e087c17c2d
--- /dev/null
+++ b/lib/api/entities/user_stars_project.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserStarsProject < Grape::Entity
+ expose :starred_since
+ expose :user, using: Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/user_status.rb b/lib/api/entities/user_status.rb
new file mode 100644
index 00000000000..9bc4cbf240f
--- /dev/null
+++ b/lib/api/entities/user_status.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserStatus < Grape::Entity
+ expose :emoji
+ expose :message
+ expose :message_html do |entity|
+ MarkupHelper.markdown_field(entity, :message)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb
new file mode 100644
index 00000000000..d3df12200ff
--- /dev/null
+++ b/lib/api/entities/user_with_admin.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserWithAdmin < UserPublic
+ expose :admin?, as: :is_admin
+ end
+ end
+end
+
+API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true)
diff --git a/lib/api/entities/variable.rb b/lib/api/entities/variable.rb
new file mode 100644
index 00000000000..6705df30b2e
--- /dev/null
+++ b/lib/api/entities/variable.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Variable < Grape::Entity
+ expose :variable_type, :key, :value
+ expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
+ expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
+ expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
+ end
+ end
+end
diff --git a/lib/api/entities/wiki_attachment.rb b/lib/api/entities/wiki_attachment.rb
new file mode 100644
index 00000000000..e622dea04dd
--- /dev/null
+++ b/lib/api/entities/wiki_attachment.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class WikiAttachment < Grape::Entity
+ include Gitlab::FileMarkdownLinkBuilder
+
+ expose :file_name
+ expose :file_path
+ expose :branch
+ expose :link do
+ expose :file_path, as: :url
+ expose :markdown do |_entity|
+ self.markdown_link
+ end
+ end
+
+ def filename
+ object.file_name
+ end
+
+ def secure_url
+ object.file_path
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb
new file mode 100644
index 00000000000..a8ef0bd857c
--- /dev/null
+++ b/lib/api/entities/wiki_page.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class WikiPage < WikiPageBasic
+ expose :content
+ end
+ end
+end
diff --git a/lib/api/entities/wiki_page_basic.rb b/lib/api/entities/wiki_page_basic.rb
new file mode 100644
index 00000000000..e10c0e6d553
--- /dev/null
+++ b/lib/api/entities/wiki_page_basic.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class WikiPageBasic < Grape::Entity
+ expose :format
+ expose :slug
+ expose :title
+ end
+ end
+end
diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb
index f92f1326daa..14888037f53 100644
--- a/lib/api/error_tracking.rb
+++ b/lib/api/error_tracking.rb
@@ -23,6 +23,34 @@ module API
present setting, with: Entities::ErrorTracking::ProjectSetting
end
+
+ desc 'Enable or disable error tracking settings for the project' do
+ detail 'This feature was introduced in GitLab 12.8.'
+ success Entities::ErrorTracking::ProjectSetting
+ end
+ params do
+ requires :active, type: Boolean, desc: 'Specifying whether to enable or disable error tracking settings', allow_blank: false
+ end
+
+ patch ':id/error_tracking/settings/' do
+ authorize! :admin_operations, user_project
+
+ setting = user_project.error_tracking_setting
+
+ not_found!('Error Tracking Setting') unless setting
+
+ update_params = {
+ error_tracking_setting_attributes: { enabled: params[:active] }
+ }
+
+ result = ::Projects::Operations::UpdateService.new(user_project, current_user, update_params).execute
+
+ if result[:status] == :success
+ present setting, with: Entities::ErrorTracking::ProjectSetting
+ else
+ result
+ end
+ end
end
end
end
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index f7ef0cfd0d8..88d04e70e11 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -28,6 +28,7 @@ module API
success ::API::Entities::Board
end
get '/:board_id' do
+ authorize!(:read_board, user_group)
present board, with: ::API::Entities::Board
end
@@ -39,6 +40,7 @@ module API
use :pagination
end
get '/' do
+ authorize!(:read_board, user_group)
present paginate(board_parent.boards.with_associations), with: Entities::Board
end
end
@@ -55,6 +57,7 @@ module API
use :pagination
end
get '/lists' do
+ authorize!(:read_board, user_group)
present paginate(board_lists), with: Entities::List
end
@@ -66,6 +69,7 @@ module API
requires :list_id, type: Integer, desc: 'The ID of a list'
end
get '/lists/:list_id' do
+ authorize!(:read_board, user_group)
present board_lists.find(params[:list_id]), with: Entities::List
end
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index abfe10b7fa1..0108f6feae3 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -59,7 +59,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Group'
- optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
+ optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
@@ -96,7 +96,7 @@ module API
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
- update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
+ update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster)
present cluster, with: Entities::ClusterGroup
diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb
index 8025a16e191..6fe72458da2 100644
--- a/lib/api/group_export.rb
+++ b/lib/api/group_export.rb
@@ -3,6 +3,8 @@
module API
class GroupExport < Grape::API
before do
+ not_found! unless Feature.enabled?(:group_import_export, user_group, default_enabled: true)
+
authorize! :admin_group, user_group
end
@@ -25,7 +27,7 @@ module API
detail 'This feature was introduced in GitLab 12.5.'
end
post ':id/export' do
- GroupExportWorker.perform_async(current_user.id, user_group.id, params)
+ GroupExportWorker.perform_async(current_user.id, user_group.id, params) # rubocop:disable CodeReuse/Worker
accepted!
end
diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb
new file mode 100644
index 00000000000..ed52506de14
--- /dev/null
+++ b/lib/api/group_import.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module API
+ class GroupImport < Grape::API
+ MAXIMUM_FILE_SIZE = 50.megabytes.freeze
+
+ helpers do
+ def parent_group
+ find_group!(params[:parent_id]) if params[:parent_id].present?
+ end
+
+ def authorize_create_group!
+ if parent_group
+ authorize! :create_subgroup, parent_group
+ else
+ authorize! :create_group
+ end
+ end
+
+ def closest_allowed_visibility_level
+ if parent_group
+ Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
+ else
+ Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+ end
+
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Workhorse authorize the group import upload' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ post 'import/authorize' do
+ require_gitlab_workhorse!
+
+ Gitlab::Workhorse.verify_api_request!(headers)
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+
+ ImportExportUploader.workhorse_authorize(has_length: false, maximum_size: MAXIMUM_FILE_SIZE)
+ end
+
+ desc 'Create a new group import' do
+ detail 'This feature was introduced in GitLab 12.8'
+ success Entities::Group
+ end
+ params do
+ requires :path, type: String, desc: 'Group path'
+ requires :name, type: String, desc: 'Group name'
+ optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace."
+ optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
+ optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
+ optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
+ optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
+ optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
+ optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
+ optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
+ end
+ post 'import' do
+ authorize_create_group!
+ require_gitlab_workhorse!
+
+ uploaded_file = UploadedFile.from_params(params, :file, ImportExportUploader.workhorse_local_upload_path)
+
+ bad_request!('Unable to process group import file') unless uploaded_file
+
+ group_params = {
+ path: params[:path],
+ name: params[:name],
+ parent_id: params[:parent_id],
+ visibility_level: closest_allowed_visibility_level,
+ import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
+ }
+
+ group = ::Groups::CreateService.new(current_user, group_params).execute
+
+ if group.persisted?
+ GroupImportWorker.perform_async(current_user.id, group.id) # rubocop:disable CodeReuse/Worker
+
+ accepted!
+ else
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 52fa3f8a68e..d375c35e8c0 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -92,6 +92,15 @@ module API
present paginate(groups), options
end
+
+ def delete_group(group)
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285')
+ destroy_conditionally!(group) do |group|
+ ::Groups::DestroyService.new(group, current_user).async_execute
+ end
+
+ accepted!
+ end
end
resource :groups do
@@ -187,12 +196,7 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285')
- destroy_conditionally!(group) do |group|
- ::Groups::DestroyService.new(group, current_user).async_execute
- end
-
- accepted!
+ delete_group(group)
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 7d9a91cd360..001fb92ec52 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -326,7 +326,7 @@ module API
def order_options_with_tie_breaker
order_options = { params[:order_by] => params[:sort] }
- order_options['id'] ||= 'desc'
+ order_options['id'] ||= params[:sort] || 'asc'
order_options
end
@@ -444,7 +444,7 @@ module API
def present_disk_file!(path, filename, content_type = 'application/octet-stream')
filename ||= File.basename(path)
- header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'attachment', filename: filename)
+ header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
header['Content-Transfer-Encoding'] = 'binary'
content_type content_type
@@ -552,7 +552,7 @@ module API
def send_git_blob(repository, blob)
env['api.format'] = :txt
content_type 'text/plain'
- header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'inline', filename: blob.name)
+ header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'inline', filename: blob.name)
# Let Workhorse examine the content and determine the better content disposition
header[Gitlab::Workhorse::DETECT_HEADER] = "true"
diff --git a/lib/api/helpers/file_upload_helpers.rb b/lib/api/helpers/file_upload_helpers.rb
new file mode 100644
index 00000000000..c5fb291a2b7
--- /dev/null
+++ b/lib/api/helpers/file_upload_helpers.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module FileUploadHelpers
+ def file_is_valid?
+ params[:file] && params[:file]['tempfile'].respond_to?(:read)
+ end
+
+ def validate_file!
+ render_api_error!('Uploaded file is invalid', 400) unless file_is_valid?
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index cc4a0d348a0..ab43096a1de 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -55,30 +55,6 @@ module API
::Users::ActivityService.new(actor).execute if commands.include?(params[:action])
end
- def merge_request_urls
- ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
- end
-
- def process_mr_push_options(push_options, project, user, changes)
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/61359')
-
- service = ::MergeRequests::PushOptionsHandlerService.new(
- project,
- user,
- changes,
- push_options
- ).execute
-
- if service.errors.present?
- push_options_warning(service.errors.join("\n\n"))
- end
- end
-
- def push_options_warning(warning)
- options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ')
- "WARNINGS:\nError encountered with push options #{options}: #{warning}"
- end
-
def redis_ping
result = Gitlab::Redis::SharedState.with { |redis| redis.ping }
@@ -104,21 +80,19 @@ module API
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project
- if params[:gl_repository]
- @project, @repo_type = Gitlab::GlRepository.parse(params[:gl_repository])
- @redirected_path = nil
- elsif params[:project]
- @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(params[:project])
- else
- @project, @repo_type, @redirected_path = nil, nil, nil
- end
+ @project, @repo_type, @redirected_path =
+ if params[:gl_repository]
+ Gitlab::GlRepository.parse(params[:gl_repository])
+ elsif params[:project]
+ Gitlab::RepoPath.parse(params[:project])
+ end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
- repo_type.identifier_for_subject(project)
+ repo_type.identifier_for_container(project)
end
def gl_project_path
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index d06c59907b4..5cc435e6801 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -46,7 +46,7 @@ module API
end
def present_members(members)
- present members, with: Entities::Member, current_user: current_user
+ present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info]
end
end
end
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index 8adfac346f6..3c453953e37 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -72,18 +72,26 @@ module API
end
def noteable_read_ability_name(noteable)
- "read_#{noteable.class.to_s.underscore}".to_sym
+ "read_#{ability_name(noteable)}".to_sym
end
- def find_noteable(parent_type, parent_id, noteable_type, noteable_id)
- params = finder_params_by_noteable_type_and_id(noteable_type, noteable_id, parent_id)
+ def ability_name(noteable)
+ if noteable.respond_to?(:to_ability_name)
+ noteable.to_ability_name
+ else
+ noteable.class.to_s.underscore
+ end
+ end
+
+ def find_noteable(noteable_type, noteable_id)
+ params = finder_params_by_noteable_type_and_id(noteable_type, noteable_id)
noteable = NotesFinder.new(current_user, params).target
noteable = nil unless can?(current_user, noteable_read_ability_name(noteable), noteable)
noteable || not_found!(noteable_type)
end
- def finder_params_by_noteable_type_and_id(type, id, parent_id)
+ def finder_params_by_noteable_type_and_id(type, id)
target_type = type.name.underscore
{ target_type: target_type }.tap do |h|
if %w(issue merge_request).include?(target_type)
@@ -92,11 +100,11 @@ module API
h[:target_id] = id
end
- add_parent_to_finder_params(h, type, parent_id)
+ add_parent_to_finder_params(h, type)
end
end
- def add_parent_to_finder_params(finder_params, noteable_type, parent_id)
+ def add_parent_to_finder_params(finder_params, noteable_type)
finder_params[:project] = user_project
end
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 5f63635297a..6bebb4bfeac 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -26,7 +26,7 @@ module API
private
def keyset_pagination_enabled?
- params[:pagination] == 'keyset' && Feature.enabled?(:api_keyset_pagination, default_enabled: true)
+ params[:pagination] == 'keyset'
end
end
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 6333e00daf5..c7c9f3ba077 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -27,7 +27,9 @@ module API
optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`'
optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`'
optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`'
+ optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`'
+ optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge'
@@ -86,6 +88,7 @@ module API
def self.update_params_at_least_one_of
[
+ :autoclose_referenced_issues,
:auto_devops_enabled,
:auto_devops_deploy_strategy,
:auto_cancel_pending_pipelines,
@@ -99,7 +102,7 @@ module API
:container_expiration_policy_attributes,
:default_branch,
:description,
- :autoclose_referenced_issues,
+ :emails_disabled,
:issues_access_level,
:lfs_enabled,
:merge_requests_access_level,
@@ -107,6 +110,7 @@ module API
:name,
:only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_pipeline_succeeds,
+ :pages_access_level,
:path,
:printing_merge_request_link_enabled,
:public_builds,
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index fa8b9ad79bd..1f1253c8542 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -45,7 +45,7 @@ module API
end
def authenticate_job!
- job = Ci::Build.find_by_id(params[:id])
+ job = current_job
validate_job!(job) do
forbidden! unless job_token_valid?(job)
@@ -54,6 +54,10 @@ module API
job
end
+ def current_job
+ @current_job ||= Ci::Build.find_by_id(params[:id])
+ end
+
def job_token_valid?(job)
token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s
token && job.valid_token?(token)
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index c02244c7202..4c44aca2de4 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -161,6 +161,7 @@ module API
def self.services
{
+ 'alerts' => [],
'asana' => [
{
required: true,
@@ -675,6 +676,12 @@ module API
type: String,
desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
},
+ {
+ required: false,
+ name: :branches_to_be_notified,
+ type: String,
+ desc: 'Branches for which notifications are to be sent'
+ },
chat_notification_flags
].flatten,
'mattermost' => [
@@ -723,6 +730,7 @@ module API
def self.service_classes
[
+ ::AlertsService,
::AsanaService,
::AssemblaService,
::BambooService,
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index d64de2bb465..382bbeb66de 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -9,7 +9,8 @@ module API
before do
Gitlab::ApplicationContext.push(
user: -> { actor&.user },
- project: -> { project }
+ project: -> { project },
+ caller_id: route.origin
)
end
@@ -211,40 +212,7 @@ module API
post '/post_receive' do
status 200
- response = Gitlab::InternalPostReceive::Response.new
-
- # Try to load the project and users so we have the application context
- # available for logging before we schedule any jobs.
- user = actor.user
- project
-
- push_options = Gitlab::PushOptions.new(params[:push_options])
-
- response.reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
-
- PostReceive.perform_async(params[:gl_repository], params[:identifier],
- params[:changes], push_options.as_json)
-
- mr_options = push_options.get(:merge_request)
- if mr_options.present?
- message = process_mr_push_options(mr_options, project, user, params[:changes])
- response.add_alert_message(message)
- end
-
- broadcast_message = BroadcastMessage.current&.last&.message
- response.add_alert_message(broadcast_message)
-
- response.add_merge_request_urls(merge_request_urls)
-
- # Neither User nor Project are guaranteed to be returned; an orphaned write deploy
- # key could be used
- if user && project
- redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)
- project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id)
-
- response.add_basic_message(redirect_message)
- response.add_basic_message(project_created_message)
- end
+ response = PostReceiveService.new(actor.user, project, params).execute
ee_post_receive_response_hook(response)
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 4e21815fa35..e5bfca13d66 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -120,6 +120,7 @@ module API
end
params do
use :issues_params
+ optional :non_archived, type: Boolean, desc: 'Return issues from non archived projects', default: true
end
get ":id/issues" do
issues = paginate(find_issues(group_id: user_group.id, include_subgroups: true))
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index bec3dc9bd97..b730e027063 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -26,7 +26,7 @@ module API
get do
authenticated_with_can_read_all_resources!
- key = KeysFinder.new(current_user, params).execute
+ key = KeysFinder.new(params).execute
not_found!('Key') unless key
diff --git a/lib/api/lsif_data.rb b/lib/api/lsif_data.rb
new file mode 100644
index 00000000000..63e6eb3ab2d
--- /dev/null
+++ b/lib/api/lsif_data.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module API
+ class LsifData < Grape::API
+ MAX_FILE_SIZE = 10.megabytes
+
+ before do
+ not_found! if Feature.disabled?(:code_navigation, user_project)
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :commit_id, type: String, desc: 'The ID of a commit'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ segment ':id/commits/:commit_id' do
+ params do
+ requires :path, type: String, desc: 'The path of a file'
+ end
+ get 'lsif/info' do
+ authorize! :download_code, user_project
+
+ artifact =
+ @project.job_artifacts
+ .with_file_types(['lsif'])
+ .for_sha(params[:commit_id])
+ .last
+
+ not_found! unless artifact
+ authorize! :read_pipeline, artifact.job.pipeline
+ file_too_large! if artifact.file.cached_size > MAX_FILE_SIZE
+
+ ::Projects::LsifDataService.new(artifact.file, @project, params).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index e4df2f341c6..2e49b4be45c 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -19,6 +19,7 @@ module API
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
+ optional :show_seat_info, type: Boolean, desc: 'Show seat information for members'
use :optional_filter_params_ee
use :pagination
end
@@ -37,6 +38,7 @@ module API
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
+ optional :show_seat_info, type: Boolean, desc: 'Show seat information for members'
use :pagination
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bd857278ee5..2b1bcc855d2 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -4,6 +4,8 @@ module API
class MergeRequests < Grape::API
include PaginationParams
+ CONTEXT_COMMITS_POST_LIMIT = 20
+
before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
@@ -139,6 +141,8 @@ module API
end
params do
use :merge_requests_params
+ optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects',
+ default: true
end
get ":id/merge_requests" do
merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true)
@@ -290,6 +294,74 @@ module API
present commits, with: Entities::Commit
end
+ desc 'Get the context commits of a merge request' do
+ success Entities::Commit
+ end
+ get ':id/merge_requests/:merge_request_iid/context_commits' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ project = merge_request.project
+
+ not_found! unless project.context_commits_enabled?
+
+ context_commits =
+ paginate(merge_request.merge_request_context_commits).map(&:to_commit)
+
+ present context_commits, with: Entities::Commit
+ end
+
+ params do
+ requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha'
+ end
+ desc 'create context commits of merge request' do
+ success Entities::Commit
+ end
+ post ':id/merge_requests/:merge_request_iid/context_commits' do
+ commit_ids = params[:commits]
+
+ if commit_ids.size > CONTEXT_COMMITS_POST_LIMIT
+ render_api_error!("Context commits array size should not be more than #{CONTEXT_COMMITS_POST_LIMIT}", 400)
+ end
+
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ project = merge_request.project
+
+ not_found! unless project.context_commits_enabled?
+
+ authorize!(:update_merge_request, merge_request)
+
+ project = merge_request.target_project
+ result = ::MergeRequests::AddContextService.new(project, current_user, merge_request: merge_request, commits: commit_ids).execute
+
+ if result.instance_of?(Array)
+ present result, with: Entities::Commit
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ params do
+ requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha'
+ end
+ desc 'remove context commits of merge request'
+ delete ':id/merge_requests/:merge_request_iid/context_commits' do
+ commit_ids = params[:commits]
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+ project = merge_request.project
+
+ not_found! unless project.context_commits_enabled?
+
+ authorize!(:destroy_merge_request, merge_request)
+ project = merge_request.target_project
+ commits = project.repository.commits_by(oids: commit_ids)
+
+ if commits.size != commit_ids.size
+ render_api_error!("One or more context commits' sha is not valid.", 400)
+ end
+
+ MergeRequestContextCommit.delete_bulk(merge_request, commits)
+ status 204
+ end
+
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 9575e8e9f36..35eda481a4f 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -30,7 +30,7 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
@@ -58,7 +58,7 @@ module API
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
get_note(noteable, params[:note_id])
end
@@ -71,7 +71,7 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
opts = {
note: params[:body],
@@ -98,7 +98,7 @@ module API
requires :body, type: String, desc: 'The content of a note'
end
put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
update_note(noteable, params[:note_id])
end
@@ -111,7 +111,7 @@ module API
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
+ noteable = find_noteable(noteable_type, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
index 1d1ef1afc6b..445a37a70c0 100644
--- a/lib/api/pipeline_schedules.rb
+++ b/lib/api/pipeline_schedules.rb
@@ -111,6 +111,25 @@ module API
destroy_conditionally!(pipeline_schedule)
end
+ desc 'Play a scheduled pipeline immediately' do
+ detail 'This feature was added in GitLab 12.8'
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/play' do
+ authorize! :play_pipeline_schedule, pipeline_schedule
+
+ job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker
+ .perform_async(pipeline_schedule.id, current_user.id)
+
+ if job_id
+ created!
+ else
+ render_api_error!('Unable to schedule pipeline run immediately', 500)
+ end
+ end
+
desc 'Create a new pipeline schedule variable' do
success Entities::Variable
end
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index 8e35914f48a..b482980b88a 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -62,7 +62,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
- optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
+ optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
use :create_params_ee
end
@@ -100,7 +100,7 @@ module API
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
- update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
+ update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster)
present cluster, with: Entities::ClusterProject
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 2b33069e324..70c913bea98 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -41,7 +41,7 @@ module API
delete ':id/registry/repositories/:repository_id', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do
authorize_admin_container_image!
- DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id)
+ DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id) # rubocop:disable CodeReuse/Worker
track_event('delete_repository')
status :accepted
@@ -79,8 +79,10 @@ module API
message = 'This request has already been made. You can run this at most once an hour for a given container repository'
render_api_error!(message, 400) unless obtain_new_cleanup_container_lease
+ # rubocop:disable CodeReuse/Worker
CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id,
- declared_params.except(:repository_id))
+ declared_params.except(:repository_id).merge(container_expiration_policy: false))
+ # rubocop:enable CodeReuse/Worker
track_event('delete_tag_bulk')
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index b3f17447ea0..ea793a09f6c 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -5,18 +5,19 @@ module API
include PaginationParams
helpers Helpers::ProjectsHelpers
+ helpers Helpers::FileUploadHelpers
helpers do
def import_params
declared_params(include_missing: false)
end
- def file_is_valid?
- import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read)
+ def throttled?(key, scope)
+ rate_limiter.throttled?(key, scope: scope)
end
- def validate_file!
- render_api_error!('The file is invalid', 400) unless file_is_valid?
+ def rate_limiter
+ ::Gitlab::ApplicationRateLimiter
end
end
@@ -43,6 +44,14 @@ module API
success Entities::ProjectImportStatus
end
post 'import' do
+ key = "project_import".to_sym
+
+ if throttled?(key, [current_user, key])
+ rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
+
+ render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
+ end
+
validate_file!
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42437')
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index ecada843972..3040c3c27c6 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -60,7 +60,7 @@ module API
mutually_exclusive :code, :content
end
post ":id/snippets" do
- authorize! :create_project_snippet, user_project
+ authorize! :create_snippet, user_project
snippet_params = declared_params(include_missing: false).merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
@@ -97,7 +97,7 @@ module API
snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
not_found!('Snippet') unless snippet
- authorize! :update_project_snippet, snippet
+ authorize! :update_snippet, snippet
snippet_params = declared_params(include_missing: false)
.merge(request: request, api: true)
@@ -126,7 +126,7 @@ module API
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
- authorize! :admin_project_snippet, snippet
+ authorize! :admin_snippet, snippet
destroy_conditionally!(snippet) do |snippet|
service = ::Snippets::DestroyService.new(current_user, snippet)
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index c7665c20234..1fd86d1e720 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -19,10 +19,15 @@ module API
end
params do
use :pagination
+ optional :search, type: String, desc: 'Search for a protected branch by name'
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/protected_branches' do
- protected_branches = user_project.protected_branches.preload(:push_access_levels, :merge_access_levels)
+ protected_branches =
+ ProtectedBranchesFinder
+ .new(user_project, params)
+ .execute
+ .preload(:push_access_levels, :merge_access_levels)
present paginate(protected_branches), with: Entities::ProtectedBranch, project: user_project
end
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 506d2b0f985..6e7a99bf0bb 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -67,6 +67,7 @@ module API
if result[:status] == :success
log_release_created_audit_event(result[:release])
+ create_evidence!
present result[:release], with: Entities::Release, current_user: current_user
else
@@ -164,6 +165,16 @@ module API
def log_release_milestones_updated_audit_event
# This is a separate method so that EE can extend its behaviour
end
+
+ def create_evidence!
+ return if release.historical_release?
+
+ if release.upcoming_release?
+ CreateEvidenceWorker.perform_at(release.released_at, release.id) # rubocop:disable CodeReuse/Worker
+ else
+ CreateEvidenceWorker.perform_async(release.id) # rubocop:disable CodeReuse/Worker
+ end
+ end
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 4106a2cdf38..00473db1ff1 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -103,8 +103,13 @@ module API
optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false
end
get ':id/repository/compare' do
- compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to], straight: params[:straight])
- present compare, with: Entities::Compare
+ compare = CompareService.new(user_project, params[:to]).execute(user_project, params[:from], straight: params[:straight])
+
+ if compare
+ present compare, with: Entities::Compare
+ else
+ not_found!("Ref")
+ end
end
desc 'Get repository contributors' do
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index 062115c5103..f7f7c881f4a 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -25,7 +25,7 @@ module API
end
get ":id/#{eventables_str}/:eventable_id/resource_label_events" do
- eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
+ eventable = find_noteable(eventable_type, params[:eventable_id])
opts = { page: params[:page], per_page: params[:per_page] }
events = ResourceLabelEventFinder.new(current_user, eventable, opts).execute
@@ -42,7 +42,8 @@ module API
requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
end
get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id" do
- eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
+ eventable = find_noteable(eventable_type, params[:eventable_id])
+
event = eventable.resource_label_events.find(params[:event_id])
not_found!('ResourceLabelEvent') unless can?(current_user, :read_resource_label_event, event)
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 60cf9bf2c9c..e1c79aa8efe 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -75,6 +75,13 @@ module API
end
resource :jobs do
+ before do
+ Gitlab::ApplicationContext.push(
+ user: -> { current_job&.user },
+ project: -> { current_job&.project }
+ )
+ end
+
desc 'Request a job' do
success Entities::JobRequest::Response
http_codes [[201, 'Job was scheduled'],
@@ -276,29 +283,8 @@ module API
bad_request!('Missing artifacts file!') unless artifacts
file_too_large! unless artifacts.size < max_artifacts_size(job)
- expire_in = params['expire_in'] ||
- Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
-
- job.job_artifacts.build(
- project: job.project,
- file: artifacts,
- file_type: params['artifact_type'],
- file_format: params['artifact_format'],
- file_sha256: artifacts.sha256,
- expire_in: expire_in)
-
- if metadata
- job.job_artifacts.build(
- project: job.project,
- file: metadata,
- file_type: :metadata,
- file_format: :gzip,
- file_sha256: metadata.sha256,
- expire_in: expire_in)
- end
-
- if job.update(artifacts_expire_in: expire_in)
- present Ci::BuildRunnerPresenter.new(job), with: Entities::JobRequest::Response
+ if Ci::CreateJobArtifactsService.new.execute(job, artifacts, params, metadata_file: metadata)
+ status :created
else
render_validation_error!(job)
end
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 50f930c7c7c..ed52a4fc8f2 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -32,10 +32,6 @@ module API
results = SearchService.new(current_user, search_params).search_objects
- process_results(results)
- end
-
- def process_results(results)
paginate(results)
end
@@ -47,7 +43,7 @@ module API
SCOPE_ENTITY[params[:scope].to_sym]
end
- def verify_search_scope!
+ def verify_search_scope!(resource:)
# In EE we have additional validation requirements for searches.
# Defining this method here as a noop allows us to easily extend it in
# EE, without having to modify this file directly.
@@ -73,7 +69,7 @@ module API
use :pagination
end
get do
- verify_search_scope!
+ verify_search_scope!(resource: nil)
check_users_search_allowed!
present search, with: entity
@@ -94,7 +90,7 @@ module API
use :pagination
end
get ':id/(-/)search' do
- verify_search_scope!
+ verify_search_scope!(resource: user_group)
check_users_search_allowed!
present search(group_id: user_group.id), with: entity
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index a7dab373b7f..b5df036c5ca 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -106,7 +106,7 @@ module API
snippet = snippets_for_current_user.find_by_id(params.delete(:id))
break not_found!('Snippet') unless snippet
- authorize! :update_personal_snippet, snippet
+ authorize! :update_snippet, snippet
attrs = declared_params(include_missing: false).merge(request: request, api: true)
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
@@ -132,7 +132,7 @@ module API
snippet = snippets_for_current_user.find_by_id(params.delete(:id))
break not_found!('Snippet') unless snippet
- authorize! :admin_personal_snippet, snippet
+ authorize! :admin_snippet, snippet
destroy_conditionally!(snippet) do |snippet|
service = ::Snippets::DestroyService.new(current_user, snippet)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index bf1fe4fc4a8..c6dc7c08b11 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -52,7 +52,9 @@ module API
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads
- optional :private_profile, type: Boolean, default: false, desc: 'Flag indicating the user has a private profile'
+ optional :theme_id, type: Integer, default: 1, desc: 'The GitLab theme for the user'
+ optional :color_scheme_id, type: Integer, default: 1, desc: 'The color scheme for the file viewer'
+ optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile'
all_or_none_of :extern_uid, :provider
use :optional_params_ee
@@ -223,6 +225,27 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc "Delete a user's identity. Available only for admins" do
+ success Entities::UserWithAdmin
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :provider, type: String, desc: 'The external provider'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ delete ":id/identities/:provider" do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ identity = user.identities.find_by(provider: params[:provider])
+ not_found!('Identity') unless identity
+
+ destroy_conditionally!(identity)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
desc 'Add an SSH key to a specified user. Available only for admins.' do
success Entities::SSHKey
end
@@ -252,17 +275,15 @@ module API
success Entities::SSHKey
end
params do
- requires :id, type: Integer, desc: 'The ID of the user'
+ requires :user_id, type: String, desc: 'The ID or username of the user'
use :pagination
end
- # rubocop: disable CodeReuse/ActiveRecord
- get ':id/keys' do
- user = User.find_by(id: params[:id])
+ get ':user_id/keys', requirements: API::USER_REQUIREMENTS do
+ user = find_user(params[:user_id])
not_found!('User') unless user && can?(current_user, :read_user, user)
present paginate(user.keys), with: Entities::SSHKey
end
- # rubocop: enable CodeReuse/ActiveRecord
desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
success Entities::SSHKey
@@ -535,6 +556,32 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Get memberships' do
+ success Entities::Membership
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The ID of the user'
+ optional :type, type: String, values: %w[Project Namespace]
+ use :pagination
+ end
+ get ":user_id/memberships" do
+ authenticated_as_admin!
+ user = find_user_by_id(params)
+
+ members = case params[:type]
+ when 'Project'
+ user.project_members
+ when 'Namespace'
+ user.group_members
+ else
+ user.members
+ end
+
+ members = members.including_source
+
+ present paginate(members), with: Entities::Membership
+ end
+
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index cb1f2fdcd17..2b6b10cf044 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -135,11 +135,11 @@ module Backup
progress.print 'Unpacking backup ... '
- unless Kernel.system(*%W(tar -xf #{tar_file}))
+ if Kernel.system(*%W(tar -xf #{tar_file}))
+ progress.puts 'done'.color(:green)
+ else
progress.puts 'unpacking backup failed'.color(:red)
exit 1
- else
- progress.puts 'done'.color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 974e32ce17c..123a695be13 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -96,6 +96,7 @@ module Backup
end
wiki = ProjectWiki.new(project)
+ wiki.repository.remove rescue nil
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_bundle)
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 0c1bbd2d250..dec4ec871f1 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -25,12 +25,10 @@ module Banzai
# * [[http://example.com/images/logo.png]]
# * [[http://example.com/images/logo.png|alt=Logo]]
#
- # - Insert a Table of Contents list:
- #
- # * [[_TOC_]]
- #
# Based on Gollum::Filter::Tags
#
+ # Note: the table of contents tag is now handled by TableOfContentsTagFilter
+ #
# Context options:
# :project_wiki (required) - Current project wiki.
#
@@ -64,23 +62,11 @@ module Banzai
def call
doc.search(".//text()").each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+ next unless node.content =~ TAGS_PATTERN
- # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
- # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
- # needs special-case handling
- if toc_tag?(node)
- process_toc_tag(node)
- else
- content = node.content
-
- next unless content =~ TAGS_PATTERN
-
- html = process_tag($1)
+ html = process_tag($1)
- if html && html != node.content
- node.replace(html)
- end
- end
+ node.replace(html) if html && html != node.content
end
doc
@@ -88,12 +74,6 @@ module Banzai
private
- # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
- # TableOfContentsFilter
- def process_toc_tag(node)
- node.parent.parent.replace(result[:toc].presence || '')
- end
-
# Process a single tag into its final HTML form.
#
# tag - The String tag contents (the stuff inside the double brackets).
@@ -129,12 +109,6 @@ module Banzai
end
end
- def toc_tag?(node)
- node.content == 'TOC' &&
- node.parent.name == 'em' &&
- node.parent.parent.text == '[[TOC]]'
- end
-
def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS
end
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
index c5a328c21b2..c1f4bf1f97f 100644
--- a/lib/banzai/filter/inline_metrics_filter.rb
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -25,7 +25,7 @@ module Banzai
# Regular expression matching metrics urls
def link_pattern
- Gitlab::Metrics::Dashboard::Url.regex
+ Gitlab::Metrics::Dashboard::Url.metrics_regex
end
private
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
index c70897fccbf..ae830831a27 100644
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -59,7 +59,7 @@ module Banzai
embed = Embed.new
url = node.attribute('data-dashboard-url').to_s
- set_path_and_permission(embed, url, URL.regex, :read_environment)
+ set_path_and_permission(embed, url, URL.metrics_regex, :read_environment)
set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission
embeds[node] = embed if embed.permission
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 9dfd77b1759..f9d8bf8a1fa 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -3,7 +3,7 @@
module Banzai
module Filter
# HTML filter that appends state information to issuable links.
- # Runs as a post-process filter as issuable state might change whilst
+ # Runs as a post-process filter as issuable state might change while
# Markdown is in the cache.
#
# This filter supports cross-project references.
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index f85be042999..09a4d71b5f6 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -31,3 +31,5 @@ module Banzai
end
end
end
+
+Banzai::Filter::IssueReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IssueReferenceFilter')
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 4c47ee4dba1..126208db935 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -121,7 +121,7 @@ module Banzai
def object_link_text(object, matches)
milestone_link = escape_once(super)
- reference = object.project&.to_reference(project)
+ reference = object.project&.to_reference_base(project)
if reference.present?
"#{milestone_link} <i>in #{reference}</i>".html_safe
diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb
index 83cf45097ed..292d4b1d56c 100644
--- a/lib/banzai/filter/project_reference_filter.rb
+++ b/lib/banzai/filter/project_reference_filter.rb
@@ -104,7 +104,7 @@ module Banzai
def link_to_project(project, link_content: nil)
url = urls.project_url(project, only_path: context[:only_path])
data = data_attribute(project: project.id)
- content = link_content || project.to_reference_with_postfix
+ content = link_content || project.to_reference
link_tag(url, data, content, project.name)
end
diff --git a/lib/banzai/filter/repository_link_filter.rb b/lib/banzai/filter/repository_link_filter.rb
index 14cd607cc50..d448238c6e4 100644
--- a/lib/banzai/filter/repository_link_filter.rb
+++ b/lib/banzai/filter/repository_link_filter.rb
@@ -101,11 +101,18 @@ module Banzai
def rebuild_relative_uri(uri)
file_path = nested_file_path_if_exists(uri)
+ resource_type = uri_type(file_path)
+
+ # Repository routes are under /-/ scope now.
+ # Since we craft a path without using route helpers we must
+ # ensure - is added here.
+ prefix = '-' if %w(tree blob raw commits).include?(resource_type.to_s)
uri.path = [
relative_url_root,
project.full_path,
- uri_type(file_path),
+ prefix,
+ resource_type,
Addressable::URI.escape(ref).gsub('#', '%23'),
Addressable::URI.escape(file_path)
].compact.join('/').squeeze('/').chomp('/')
diff --git a/lib/banzai/filter/table_of_contents_tag_filter.rb b/lib/banzai/filter/table_of_contents_tag_filter.rb
new file mode 100644
index 00000000000..13d0a6a4cc7
--- /dev/null
+++ b/lib/banzai/filter/table_of_contents_tag_filter.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # Using `[[_TOC_]]`, inserts a Table of Contents list.
+ # This syntax is based on the Gollum syntax. This way we have
+ # some consistency between with wiki and normal markdown.
+ # If there ever emerges a markdown standard, we can implement
+ # that here.
+ #
+ # The support for this has been removed from GollumTagsFilter
+ #
+ # Based on Banzai::Filter::GollumTagsFilter
+ class TableOfContentsTagFilter < HTML::Pipeline::Filter
+ TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')])
+
+ def call
+ return doc if context[:no_header_anchors]
+
+ doc.xpath(TEXT_QUERY).each do |node|
+ # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
+ # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
+ # needs special-case handling
+ process_toc_tag(node) if toc_tag?(node)
+ end
+
+ doc
+ end
+
+ private
+
+ # Replace an entire `[[<em>TOC</em>]]` node with the result generated by
+ # TableOfContentsFilter
+ def process_toc_tag(node)
+ node.parent.parent.replace(result[:toc].presence || '')
+ end
+
+ def toc_tag?(node)
+ node.content == 'TOC' &&
+ node.parent.name == 'em' &&
+ node.parent.parent.text == '[[TOC]]'
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index f6c12cdb53b..dad0d95685e 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -32,6 +32,7 @@ module Banzai
Filter::InlineMetricsFilter,
Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
+ Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
Filter::SuggestionFilter,
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
index 6f6ac08de04..b86c259efbd 100644
--- a/lib/banzai/reference_parser/snippet_parser.rb
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -12,7 +12,7 @@ module Banzai
private
def can_read_reference?(user, ref_project, node)
- can?(user, :read_project_snippet, referenced_by([node]).first)
+ can?(user, :read_snippet, referenced_by([node]).first)
end
end
end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index d41490d2ebd..3e9cf2ab320 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -4,7 +4,7 @@ module Constraints
class ProjectUrlConstrainer
def matches?(request, existence_check: true)
namespace_path = request.params[:namespace_id]
- project_path = request.params[:project_id] || request.params[:id]
+ project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id]
full_path = [namespace_path, project_path].join('/')
return false unless ProjectPathValidator.valid_path?(full_path)
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index bc0347f6ea1..12f7f04634f 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -6,6 +6,8 @@ require 'digest'
module ContainerRegistry
class Client
+ include Gitlab::Utils::StrongMemoize
+
attr_accessor :uri
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
@@ -35,10 +37,25 @@ module ContainerRegistry
response.headers['docker-content-digest'] if response.success?
end
- def delete_repository_tag(name, reference)
- result = faraday.delete("/v2/#{name}/manifests/#{reference}")
+ def delete_repository_tag_by_digest(name, reference)
+ delete_if_exists("/v2/#{name}/manifests/#{reference}")
+ end
- result.success? || result.status == 404
+ def delete_repository_tag_by_name(name, reference)
+ delete_if_exists("/v2/#{name}/tags/reference/#{reference}")
+ end
+
+ # Check if the registry supports tag deletion. This is only supported by the
+ # GitLab registry fork. The fastest and safest way to check this is to send
+ # an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random
+ # repository name and tag (the registry won't check if they exist).
+ # Registries that support tag deletion will reply with a 200 OK and include
+ # the DELETE method in the Allow header. Others reply with an 404 Not Found.
+ def supports_tag_delete?
+ strong_memoize(:supports_tag_delete) do
+ response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {})
+ response.success? && response.headers['allow']&.include?('DELETE')
+ end
end
def upload_raw_blob(path, blob)
@@ -86,9 +103,7 @@ module ContainerRegistry
end
def delete_blob(name, digest)
- result = faraday.delete("/v2/#{name}/blobs/#{digest}")
-
- result.success? || result.status == 404
+ delete_if_exists("/v2/#{name}/blobs/#{digest}")
end
def put_tag(name, reference, manifest)
@@ -163,6 +178,12 @@ module ContainerRegistry
conn.adapter :net_http
end
end
+
+ def delete_if_exists(path)
+ result = faraday.delete(path)
+
+ result.success? || result.status == 404
+ end
end
end
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 3c308258a3f..e1a2891e43a 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -118,7 +118,7 @@ module ContainerRegistry
def unsafe_delete
return unless digest
- client.delete_repository_tag(repository.path, digest)
+ client.delete_repository_tag_by_digest(repository.path, digest)
end
end
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index f739fe5e16e..70666dc7924 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -33,6 +33,7 @@ module DeclarativePolicy
attr_reader :steps
def initialize(steps)
@steps = steps
+ @state = nil
end
# We make sure only to run any given Runner once,
diff --git a/lib/feature.rb b/lib/feature.rb
index 543512b1598..aadc2c64957 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -32,6 +32,8 @@ class Feature
end
def persisted_names
+ return [] unless Gitlab::Database.exists?
+
Gitlab::SafeRequestStore[:flipper_persisted_names] ||=
begin
# We saw on GitLab.com, this database request was called 2300
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 2bd55c36a03..d327162b34e 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -1,33 +1,25 @@
# frozen_string_literal: true
-require 'set'
-
class Feature
class Gitaly
- # Server feature flags should use '_' to separate words.
- SERVER_FEATURE_FLAGS =
- %w[
- cache_invalidator
- inforef_uploadpack_cache
- get_tag_messages_go
- filter_shas_with_signatures_go
- ].freeze
-
- DEFAULT_ON_FLAGS = Set.new([]).freeze
+ PREFIX = "gitaly_"
class << self
def enabled?(feature_flag)
return false unless Feature::FlipperFeature.table_exists?
- default_on = DEFAULT_ON_FLAGS.include?(feature_flag)
- Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on)
+ Feature.enabled?("#{PREFIX}#{feature_flag}")
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end
def server_feature_flags
- SERVER_FEATURE_FLAGS.map do |f|
- ["gitaly-feature-#{f.tr('_', '-')}", enabled?(f).to_s]
+ Feature.persisted_names
+ .select { |f| f.start_with?(PREFIX) }
+ .map do |f|
+ flag = f.delete_prefix(PREFIX)
+
+ ["gitaly-feature-#{flag.tr('_', '-')}", enabled?(flag).to_s]
end.to_h
end
end
diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb
index 64ab5db4fcd..89a836e629f 100644
--- a/lib/gitaly/server.rb
+++ b/lib/gitaly/server.rb
@@ -53,6 +53,20 @@ module Gitaly
storage_status&.fs_type
end
+ def disk_used
+ disk_statistics_storage_status&.used
+ end
+
+ def disk_available
+ disk_statistics_storage_status&.available
+ end
+
+ # Simple convenience method for when obtaining both used and available
+ # statistics at once is preferred.
+ def disk_stats
+ disk_statistics_storage_status
+ end
+
def address
Gitlab::GitalyClient.address(@storage)
rescue RuntimeError => e
@@ -65,6 +79,10 @@ module Gitaly
@storage_status ||= info.storage_statuses.find { |s| s.storage_name == storage }
end
+ def disk_statistics_storage_status
+ @disk_statistics_storage_status ||= disk_statistics.storage_statuses.find { |s| s.storage_name == storage }
+ end
+
def matches_sha?
match = server_version.match(SHA_VERSION_REGEX)
return false unless match
@@ -76,7 +94,19 @@ module Gitaly
@info ||=
begin
Gitlab::GitalyClient::ServerService.new(@storage).info
- rescue GRPC::Unavailable, GRPC::DeadlineExceeded
+ rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex
+ Gitlab::ErrorTracking.track_exception(ex)
+ # This will show the server as being out of date
+ Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: [])
+ end
+ end
+
+ def disk_statistics
+ @disk_statistics ||=
+ begin
+ Gitlab::GitalyClient::ServerService.new(@storage).disk_statistics
+ rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex
+ Gitlab::ErrorTracking.track_exception(ex)
# This will show the server as being out of date
Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: [])
end
diff --git a/lib/gitlab/action_view_output/context.rb b/lib/gitlab/action_view_output/context.rb
deleted file mode 100644
index 9fbc9811636..00000000000
--- a/lib/gitlab/action_view_output/context.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-# This file was simplified from https://raw.githubusercontent.com/rails/rails/195f39804a7a4a0034f25e8704220e03d95a752a/actionview/lib/action_view/context.rb.
-#
-# It is only needed by modules that need to call ActionView helper
-# methods (e.g. those in
-# https://github.com/rails/rails/tree/c4d3e202e10ae627b3b9c34498afb45450652421/actionview/lib/action_view/helpers)
-# to generate tags outside of a Rails controller (e.g. API, Sidekiq,
-# etc.).
-#
-# In Rails 5, ActionView::Context automatically includes CompiledTemplates.
-# This means that any module that includes ActionView::Context is now a descendant
-# of CompiledTemplates.
-#
-# When a partial is rendered for the first time, it runs
-# Module#module_eval, which will evaluate a string source that defines a
-# new method. For example:
-#
-# def _app_views_profiles_show_html_haml___1285955918103175884_70307801785400(local_assigns, output_buffer)
-# "hello world"
-# end
-#
-# When a new method is defined, the Ruby interpreter clears the method
-# cache for all descendants, and all methods for those modules will have
-# to be redefined. This can lead to a significant performance penalty.
-#
-# Rails 6 fixes this behavior by moving out the `include
-# CompiledTemplates` into ActionView::Base so that including `ActionView::Context`
-# doesn't quietly affect other modules in this way.
-
-if Rails::VERSION::STRING.start_with?('6')
- raise 'This module is no longer needed in Rails 6. Use ActionView::Context instead.'
-end
-
-module Gitlab
- module ActionViewOutput
- module Context
- attr_accessor :output_buffer, :view_flow
- end
- end
-end
diff --git a/lib/gitlab/alerting/alert.rb b/lib/gitlab/alerting/alert.rb
new file mode 100644
index 00000000000..531307b93d4
--- /dev/null
+++ b/lib/gitlab/alerting/alert.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Alerting
+ class Alert
+ include ActiveModel::Model
+ include Gitlab::Utils::StrongMemoize
+ include Presentable
+
+ attr_accessor :project, :payload
+
+ def gitlab_alert
+ strong_memoize(:gitlab_alert) do
+ parse_gitlab_alert_from_payload
+ end
+ end
+
+ def metric_id
+ strong_memoize(:metric_id) do
+ payload&.dig('labels', 'gitlab_alert_id')
+ end
+ end
+
+ def title
+ strong_memoize(:title) do
+ gitlab_alert&.title || parse_title_from_payload
+ end
+ end
+
+ def description
+ strong_memoize(:description) do
+ parse_description_from_payload
+ end
+ end
+
+ def environment
+ strong_memoize(:environment) do
+ gitlab_alert&.environment || parse_environment_from_payload
+ end
+ end
+
+ def annotations
+ strong_memoize(:annotations) do
+ parse_annotations_from_payload || []
+ end
+ end
+
+ def starts_at
+ strong_memoize(:starts_at) do
+ parse_datetime_from_payload('startsAt')
+ end
+ end
+
+ def starts_at_raw
+ strong_memoize(:starts_at_raw) do
+ payload&.dig('startsAt')
+ end
+ end
+
+ def ends_at
+ strong_memoize(:ends_at) do
+ parse_datetime_from_payload('endsAt')
+ end
+ end
+
+ def full_query
+ strong_memoize(:full_query) do
+ gitlab_alert&.full_query || parse_expr_from_payload
+ end
+ end
+
+ def alert_markdown
+ strong_memoize(:alert_markdown) do
+ parse_alert_markdown_from_payload
+ end
+ end
+
+ def status
+ strong_memoize(:status) do
+ payload&.dig('status')
+ end
+ end
+
+ def firing?
+ status == 'firing'
+ end
+
+ def resolved?
+ status == 'resolved'
+ end
+
+ def gitlab_managed?
+ metric_id.present?
+ end
+
+ def valid?
+ payload.respond_to?(:dig) && project && title && starts_at
+ end
+
+ def present
+ super(presenter_class: Projects::Prometheus::AlertPresenter)
+ end
+
+ private
+
+ def parse_environment_from_payload
+ environment_name = payload&.dig('labels', 'gitlab_environment_name')
+
+ return unless environment_name
+
+ EnvironmentsFinder.new(project, nil, { name: environment_name })
+ .find
+ &.first
+ end
+
+ def parse_gitlab_alert_from_payload
+ return unless metric_id
+
+ Projects::Prometheus::AlertsFinder
+ .new(project: project, metric: metric_id)
+ .execute
+ .first
+ end
+
+ def parse_title_from_payload
+ payload&.dig('annotations', 'title') ||
+ payload&.dig('annotations', 'summary') ||
+ payload&.dig('labels', 'alertname')
+ end
+
+ def parse_description_from_payload
+ payload&.dig('annotations', 'description')
+ end
+
+ def parse_annotations_from_payload
+ payload&.dig('annotations')&.map do |label, value|
+ Alerting::AlertAnnotation.new(label: label, value: value)
+ end
+ end
+
+ def parse_datetime_from_payload(field)
+ value = payload&.dig(field)
+ return unless value
+
+ Time.rfc3339(value)
+ rescue ArgumentError
+ end
+
+ # Parses `g0.expr` from `generatorURL`.
+ #
+ # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1
+ def parse_expr_from_payload
+ url = payload&.dig('generatorURL')
+ return unless url
+
+ uri = URI(url)
+
+ Rack::Utils.parse_query(uri.query).fetch('g0.expr')
+ rescue URI::InvalidURIError, KeyError
+ end
+
+ def parse_alert_markdown_from_payload
+ payload&.dig('annotations', 'gitlab_incident_markdown')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alerting/alert_annotation.rb b/lib/gitlab/alerting/alert_annotation.rb
new file mode 100644
index 00000000000..a4b3a97b08c
--- /dev/null
+++ b/lib/gitlab/alerting/alert_annotation.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Alerting
+ class AlertAnnotation
+ include ActiveModel::Model
+
+ attr_accessor :label, :value
+ end
+ end
+end
diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb
new file mode 100644
index 00000000000..a54bb44d66a
--- /dev/null
+++ b/lib/gitlab/alerting/notification_payload_parser.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Alerting
+ class NotificationPayloadParser
+ BadPayloadError = Class.new(StandardError)
+
+ DEFAULT_TITLE = 'New: Incident'
+
+ def initialize(payload)
+ @payload = payload.to_h.with_indifferent_access
+ end
+
+ def self.call(payload)
+ new(payload).call
+ end
+
+ def call
+ {
+ 'annotations' => annotations,
+ 'startsAt' => starts_at
+ }.compact
+ end
+
+ private
+
+ attr_reader :payload
+
+ def title
+ payload[:title].presence || DEFAULT_TITLE
+ end
+
+ def annotations
+ primary_params
+ .reverse_merge(flatten_secondary_params)
+ .transform_values(&:presence)
+ .compact
+ end
+
+ def primary_params
+ {
+ 'title' => title,
+ 'description' => payload[:description],
+ 'monitoring_tool' => payload[:monitoring_tool],
+ 'service' => payload[:service],
+ 'hosts' => hosts.presence
+ }
+ end
+
+ def hosts
+ Array(payload[:hosts]).reject(&:blank?)
+ end
+
+ def current_time
+ Time.current.change(usec: 0).rfc3339
+ end
+
+ def starts_at
+ Time.parse(payload[:start_time].to_s).rfc3339
+ rescue ArgumentError
+ current_time
+ end
+
+ def secondary_params
+ payload.except(:start_time)
+ end
+
+ def flatten_secondary_params
+ Gitlab::Utils::SafeInlineHash.merge_keys!(secondary_params)
+ rescue ArgumentError
+ raise BadPayloadError, 'The payload is too big'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
index 8e70236ce75..79e60e28fc7 100644
--- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb
+++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# This module represents the default Cycle Analytics stages that are currently provided by CE
+# This module represents the default Value Stream Analytics stages that are currently provided by CE
# Each method returns a hash that can be used to build a new stage object.
#
# Example:
diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb
index 71dbfea70e8..b950bfb0f3a 100644
--- a/lib/gitlab/application_context.rb
+++ b/lib/gitlab/application_context.rb
@@ -5,17 +5,18 @@ module Gitlab
class ApplicationContext
include Gitlab::Utils::LazyAttributes
- Attribute = Struct.new(:name, :type)
+ Attribute = Struct.new(:name, :type, :evaluation)
APPLICATION_ATTRIBUTES = [
Attribute.new(:project, Project),
Attribute.new(:namespace, Namespace),
- Attribute.new(:user, User)
+ Attribute.new(:user, User),
+ Attribute.new(:caller_id, String)
].freeze
def self.with_context(args, &block)
application_context = new(**args)
- Labkit::Context.with_context(application_context.to_lazy_hash, &block)
+ application_context.use(&block)
end
def self.push(args)
@@ -37,9 +38,14 @@ module Gitlab
hash[:user] = -> { username } if set_values.include?(:user)
hash[:project] = -> { project_path } if set_values.include?(:project)
hash[:root_namespace] = -> { root_namespace_path } if include_namespace?
+ hash[:caller_id] = caller_id if set_values.include?(:caller_id)
end
end
+ def use
+ Labkit::Context.with_context(to_lazy_hash) { yield }
+ end
+
private
attr_reader :set_values
@@ -75,3 +81,5 @@ module Gitlab
end
end
end
+
+Gitlab::ApplicationContext.prepend_if_ee('EE::Gitlab::ApplicationContext')
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index 629632b744b..49e1f1edfb9 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -22,6 +22,7 @@ module Gitlab
project_export: { threshold: 1, interval: 5.minutes },
project_download_export: { threshold: 10, interval: 10.minutes },
project_generate_new_export: { threshold: 1, interval: 5.minutes },
+ project_import: { threshold: 30, interval: 10.minutes },
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
show_raw_controller: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.raw_blob_request_limit }, interval: 1.minute }
}.freeze
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 821c68dbedc..1329357d0b8 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -49,7 +49,7 @@ module Gitlab
lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
- deploy_token_check(login, password) ||
+ deploy_token_check(login, password, project) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
@@ -208,7 +208,7 @@ module Gitlab
end.uniq
end
- def deploy_token_check(login, password)
+ def deploy_token_check(login, password, project)
return unless password.present?
token = DeployToken.active.find_by_token(password)
@@ -219,7 +219,7 @@ module Gitlab
scopes = abilities_for_scopes(token.scopes)
if valid_scoped_token?(token, all_available_scopes)
- Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes)
+ Gitlab::Auth::Result.new(token, project, :deploy_token, scopes)
end
end
diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb
index cb39baaa6cc..1ef95c03cfc 100644
--- a/lib/gitlab/auth/current_user_mode.rb
+++ b/lib/gitlab/auth/current_user_mode.rb
@@ -10,12 +10,54 @@ module Gitlab
class CurrentUserMode
NotRequestedError = Class.new(StandardError)
+ # RequestStore entries
+ CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
+ CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY = { res: :current_user_mode, data: :current_admin }.freeze
+
+ # SessionStore entries
SESSION_STORE_KEY = :current_user_mode
- ADMIN_MODE_START_TIME_KEY = 'admin_mode'
- ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
+ ADMIN_MODE_START_TIME_KEY = :admin_mode
+ ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
MAX_ADMIN_MODE_TIME = 6.hours
ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
+ class << self
+ # Admin mode activation requires storing a flag in the user session. Using this
+ # method when scheduling jobs in Sidekiq will bypass the session check for a
+ # user that was already in admin mode
+ def bypass_session!(admin_id)
+ Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
+
+ Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")
+
+ yield
+ ensure
+ Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
+ end
+
+ def bypass_session_admin_id
+ Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
+ end
+
+ # Store in the current request the provided user model (only if in admin mode)
+ # and yield
+ def with_current_admin(admin)
+ return yield unless self.new(admin).admin_mode?
+
+ Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin
+
+ Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")
+
+ yield
+ ensure
+ Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
+ end
+
+ def current_admin
+ Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
+ end
+ end
+
def initialize(user)
@user = user
end
@@ -42,7 +84,7 @@ module Gitlab
raise NotRequestedError unless admin_mode_requested?
- reset_request_store
+ reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
@@ -55,7 +97,7 @@ module Gitlab
def disable_admin_mode!
return unless user&.admin?
- reset_request_store
+ reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
@@ -64,7 +106,7 @@ module Gitlab
def request_admin_mode!
return unless user&.admin?
- reset_request_store
+ reset_request_store_cache_entries
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end
@@ -73,10 +115,12 @@ module Gitlab
attr_reader :user
+ # RequestStore entry to cache #admin_mode? result
def admin_mode_rs_key
@admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
end
+ # RequestStore entry to cache #admin_mode_requested? result
def admin_mode_requested_rs_key
@admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
end
@@ -86,6 +130,7 @@ module Gitlab
end
def any_session_with_admin_mode?
+ return true if bypass_session?
return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
all_sessions.any? do |session|
@@ -103,7 +148,11 @@ module Gitlab
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
end
- def reset_request_store
+ def bypass_session?
+ user&.id && user.id == self.class.bypass_session_admin_id
+ end
+
+ def reset_request_store_cache_entries
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
end
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index b8ed740e08c..940b802be7e 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -49,7 +49,7 @@ module Gitlab
return true
end
- # Block user in GitLab if he/she was blocked in AD
+ # Block user in GitLab if they were blocked in AD
if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter)
block_user(user, 'is disabled in Active Directory')
false
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
index b0df9757bbd..a2b0dfd5c66 100644
--- a/lib/gitlab/auth/saml/auth_hash.rb
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def extract_authn_context(document)
- REXML::XPath.first(document, "//*[name()='saml:AuthnStatement' or name()='saml2:AuthnStatement']/*[name()='saml:AuthnContext' or name()='saml2:AuthnContext']/*[name()='saml:AuthnContextClassRef' or name()='saml2:AuthnContextClassRef']/text()").to_s
+ REXML::XPath.first(document, "//*[name()='saml:AuthnStatement' or name()='saml2:AuthnStatement' or name()='AuthnStatement']/*[name()='saml:AuthnContext' or name()='saml2:AuthnContext' or name()='AuthnContext']/*[name()='saml:AuthnContextClassRef' or name()='saml2:AuthnContextClassRef' or name()='AuthnContextClassRef']/text()").to_s
end
end
end
diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb
index ddd6b11eebb..6a16c37e880 100644
--- a/lib/gitlab/background_migration.rb
+++ b/lib/gitlab/background_migration.rb
@@ -58,6 +58,14 @@ module Gitlab
migration_class_for(class_name).new.perform(*arguments)
end
+ def self.remaining
+ scheduled = Sidekiq::ScheduledSet.new.count do |job|
+ job.queue == self.queue
+ end
+
+ scheduled + Sidekiq::Queue.new(self.queue).size
+ end
+
def self.exists?(migration_class, additional_queues = [])
enqueued = Sidekiq::Queue.new(self.queue)
scheduled = Sidekiq::ScheduledSet.new
diff --git a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb b/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb
deleted file mode 100644
index 19f5821d449..00000000000
--- a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Create missing PrometheusServices records or sets active attribute to true
- # for all projects which belongs to cluster with Prometheus Application installed.
- class ActivatePrometheusServicesForSharedClusterApplications
- module Migratable
- # Migration model namespace isolated from application code.
- class PrometheusService < ActiveRecord::Base
- self.inheritance_column = :_type_disabled
- self.table_name = 'services'
-
- default_scope { where("services.type = 'PrometheusService'") }
-
- def self.for_project(project_id)
- new(
- project_id: project_id,
- active: true,
- properties: '{}',
- type: 'PrometheusService',
- template: false,
- push_events: true,
- issues_events: true,
- merge_requests_events: true,
- tag_push_events: true,
- note_events: true,
- category: 'monitoring',
- default: false,
- wiki_page_events: true,
- pipeline_events: true,
- confidential_issues_events: true,
- commit_events: true,
- job_events: true,
- confidential_note_events: true,
- deployment_events: false
- )
- end
-
- def managed?
- properties == '{}'
- end
- end
- end
-
- def perform(project_id)
- service = Migratable::PrometheusService.find_by(project_id: project_id) || Migratable::PrometheusService.for_project(project_id)
- service.update!(active: true) if service.managed?
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
index 3c142327e94..2a079060380 100644
--- a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
+++ b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
@@ -11,7 +11,7 @@ module Gitlab
module Storage
# Class that returns the disk path for a project using hashed storage
- class HashedProject
+ class Hashed
attr_accessor :project
ROOT_PATH_PREFIX = '@hashed'
@@ -121,7 +121,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
- Storage::HashedProject.new(self)
+ Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb
index 1d9aa050041..263546bd132 100644
--- a/lib/gitlab/background_migration/backfill_project_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_project_repositories.rb
@@ -46,7 +46,7 @@ module Gitlab
module Storage
# Class that returns the disk path for a project using hashed storage
- class HashedProject
+ class Hashed
attr_accessor :project
ROOT_PATH_PREFIX = '@hashed'
@@ -176,7 +176,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
- Storage::HashedProject.new(self)
+ Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
diff --git a/lib/gitlab/background_migration/backfill_project_settings.rb b/lib/gitlab/background_migration/backfill_project_settings.rb
new file mode 100644
index 00000000000..7d7f19e1fda
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_settings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Backfill project_settings for a range of projects
+ class BackfillProjectSettings
+ def perform(start_id, end_id)
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO project_settings (project_id, created_at, updated_at)
+ SELECT projects.id, now(), now()
+ FROM projects
+ WHERE projects.id BETWEEN #{start_id} AND #{end_id}
+ ON CONFLICT (project_id) DO NOTHING;
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb
new file mode 100644
index 00000000000..d6ec56ae19e
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # No OP for CE
+ class FixOrphanPromotedIssues
+ def perform(note_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::FixOrphanPromotedIssues.prepend_if_ee('EE::Gitlab::BackgroundMigration::FixOrphanPromotedIssues')
diff --git a/lib/gitlab/background_migration/fix_projects_without_project_feature.rb b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb
new file mode 100644
index 00000000000..68665db522e
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This migration creates missing project_features records
+ # for the projects within the given range of ids
+ class FixProjectsWithoutProjectFeature
+ def perform(from_id, to_id)
+ if number_of_created_records = create_missing!(from_id, to_id) > 0
+ log(number_of_created_records, from_id, to_id)
+ end
+ end
+
+ private
+
+ def create_missing!(from_id, to_id)
+ result = ActiveRecord::Base.connection.select_one(sql(from_id, to_id))
+ return 0 unless result
+
+ result['number_of_created_records']
+ end
+
+ def sql(from_id, to_id)
+ <<~SQL
+ WITH created_records AS (
+ INSERT INTO project_features (
+ project_id,
+ merge_requests_access_level,
+ issues_access_level,
+ wiki_access_level,
+ snippets_access_level,
+ builds_access_level,
+ repository_access_level,
+ forking_access_level,
+ pages_access_level,
+ created_at,
+ updated_at
+ )
+ SELECT projects.id,
+ 20, 20, 20, 20, 20, 20, 20,
+ #{pages_access_level},
+ TIMEZONE('UTC', NOW()), TIMEZONE('UTC', NOW())
+ FROM projects
+ WHERE projects.id BETWEEN #{Integer(from_id)} AND #{Integer(to_id)}
+ AND NOT EXISTS (
+ SELECT 1 FROM project_features
+ WHERE project_features.project_id = projects.id
+ )
+ ON CONFLICT (project_id) DO NOTHING
+ RETURNING *
+ )
+ SELECT COUNT(*) as number_of_created_records
+ FROM created_records
+ SQL
+ end
+
+ def pages_access_level
+ if ::Gitlab::Pages.access_control_is_forced?
+ "10"
+ else
+ "GREATEST(projects.visibility_level, 10)"
+ end
+ end
+
+ def log(count, from_id, to_id)
+ logger = Gitlab::BackgroundMigration::Logger.build
+
+ logger.info(message: "FixProjectsWithoutProjectFeature: created missing project_features for #{count} projects in id=#{from_id}...#{to_id}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/link_lfs_objects.rb b/lib/gitlab/background_migration/link_lfs_objects.rb
new file mode 100644
index 00000000000..69c03f617bf
--- /dev/null
+++ b/lib/gitlab/background_migration/link_lfs_objects.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Create missing LfsObjectsProject records for forks
+ class LinkLfsObjects
+ # Model definition used for migration
+ class ForkNetworkMember < ActiveRecord::Base
+ self.table_name = 'fork_network_members'
+
+ def self.with_non_existing_lfs_objects
+ joins('JOIN lfs_objects_projects lop ON fork_network_members.forked_from_project_id = lop.project_id')
+ .where(
+ <<~SQL
+ NOT EXISTS (
+ SELECT 1
+ FROM lfs_objects_projects
+ WHERE lfs_objects_projects.project_id = fork_network_members.project_id
+ AND lfs_objects_projects.lfs_object_id = lop.lfs_object_id
+ )
+ SQL
+ )
+ end
+ end
+
+ def perform(start_id, end_id)
+ # no-op as some queries times out
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb b/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb
new file mode 100644
index 00000000000..9e330f7c008
--- /dev/null
+++ b/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This background migration updates children of group to match visibility of a parent
+ class UpdateExistingSubgroupToMatchVisibilityLevelOfParent
+ def perform(parents_groups_ids, level)
+ groups_ids = Gitlab::ObjectHierarchy.new(Group.where(id: parents_groups_ids))
+ .base_and_descendants
+ .where("visibility_level > ?", level)
+ .select(:id)
+
+ return if groups_ids.empty?
+
+ Group
+ .where(id: groups_ids)
+ .update_all(visibility_level: level)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb
new file mode 100644
index 00000000000..40f45301727
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ class CreateResourceUserMention
+ # Resources that have mentions to be migrated:
+ # issue, merge_request, epic, commit, snippet, design
+
+ BULK_INSERT_SIZE = 5000
+ ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models'
+
+ def perform(resource_model, join, conditions, with_notes, start_id, end_id)
+ resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String)
+ model = with_notes ? Gitlab::BackgroundMigration::UserMentions::Models::Note : resource_model
+ resource_user_mention_model = resource_model.user_mention_model
+
+ records = model.joins(join).where(conditions).where(id: start_id..end_id)
+
+ records.in_groups_of(BULK_INSERT_SIZE, false).each do |records|
+ mentions = []
+ records.each do |record|
+ mentions << record.build_mention_values(resource_user_mention_model.resource_foreign_key)
+ end
+
+ Gitlab::Database.bulk_insert(
+ resource_user_mention_model.table_name,
+ mentions,
+ return_ids: true,
+ disable_quote: resource_model.no_quote_columns,
+ on_conflict: :do_nothing
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb
new file mode 100644
index 00000000000..b7fa92a6686
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ # == IsolatedMentionable concern
+ #
+ # Shortcutted for isolation version of Mentionable to be used in mentions migrations
+ #
+ module IsolatedMentionable
+ extend ::ActiveSupport::Concern
+
+ class_methods do
+ # Indicate which attributes of the Mentionable to search for GFM references.
+ def attr_mentionable(attr, options = {})
+ attr = attr.to_s
+ mentionable_attrs << [attr, options]
+ end
+ end
+
+ included do
+ # Accessor for attributes marked mentionable.
+ cattr_accessor :mentionable_attrs, instance_accessor: false do
+ []
+ end
+
+ if self < Participable
+ participant -> (user, ext) { all_references(user, extractor: ext) }
+ end
+ end
+
+ def all_references(current_user = nil, extractor: nil)
+ # Use custom extractor if it's passed in the function parameters.
+ if extractor
+ extractors[current_user] = extractor
+ else
+ extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user)
+
+ extractor.reset_memoized_values
+ end
+
+ self.class.mentionable_attrs.each do |attr, options|
+ text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
+ options = options.merge(
+ cache_key: [self, attr],
+ author: author,
+ skip_project_check: skip_project_check?
+ ).merge(mentionable_params)
+
+ cached_html = self.try(:updated_cached_html_for, attr.to_sym)
+ options[:rendered] = cached_html if cached_html
+
+ extractor.analyze(text, options)
+ end
+
+ extractor
+ end
+
+ def extractors
+ @extractors ||= {}
+ end
+
+ def skip_project_check?
+ false
+ end
+
+ def build_mention_values(resource_foreign_key)
+ refs = all_references(author)
+
+ {
+ "#{resource_foreign_key}": user_mention_resource_id,
+ note_id: user_mention_note_id,
+ mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)),
+ mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)),
+ mentioned_groups_ids: array_to_sql(refs.mentioned_groups.pluck(:id))
+ }
+ end
+
+ def array_to_sql(ids_array)
+ return unless ids_array.present?
+
+ '{' + ids_array.join(", ") + '}'
+ end
+
+ private
+
+ def mentionable_params
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb
new file mode 100644
index 00000000000..fa479cb0ed3
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ # Extract common no_quote_columns method used in determining the columns that do not need
+ # to be quoted for corresponding models
+ module MentionableMigrationMethods
+ extend ::ActiveSupport::Concern
+
+ class_methods do
+ def no_quote_columns
+ [
+ :note_id,
+ user_mention_model.resource_foreign_key
+ ]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/epic.rb b/lib/gitlab/background_migration/user_mentions/models/epic.rb
new file mode 100644
index 00000000000..9797c86478e
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/epic.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ class Epic < ActiveRecord::Base
+ include IsolatedMentionable
+ include CacheMarkdownField
+ include MentionableMigrationMethods
+
+ attr_mentionable :title, pipeline: :single_line
+ attr_mentionable :description
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description, issuable_state_filter_enabled: true
+
+ self.table_name = 'epics'
+
+ belongs_to :author, class_name: "User"
+ belongs_to :project
+ belongs_to :group
+
+ def self.user_mention_model
+ Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention
+ end
+
+ def user_mention_model
+ self.class.user_mention_model
+ end
+
+ def project
+ nil
+ end
+
+ def mentionable_params
+ { group: group, label_url_method: :group_epics_url }
+ end
+
+ def user_mention_resource_id
+ id
+ end
+
+ def user_mention_note_id
+ 'NULL'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb
new file mode 100644
index 00000000000..4e3ce9bf3a7
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ class EpicUserMention < ActiveRecord::Base
+ self.table_name = 'epic_user_mentions'
+
+ def self.resource_foreign_key
+ :epic_id
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/user_mentions/models/note.rb b/lib/gitlab/background_migration/user_mentions/models/note.rb
new file mode 100644
index 00000000000..dc364d7af5a
--- /dev/null
+++ b/lib/gitlab/background_migration/user_mentions/models/note.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ module UserMentions
+ module Models
+ class Note < ActiveRecord::Base
+ include IsolatedMentionable
+ include CacheMarkdownField
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+
+ attr_mentionable :note, pipeline: :note
+ cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
+
+ belongs_to :author, class_name: "User"
+ belongs_to :noteable, polymorphic: true
+ belongs_to :project
+
+ def for_personal_snippet?
+ noteable.class.name == 'PersonalSnippet'
+ end
+
+ def for_project_noteable?
+ !for_personal_snippet?
+ end
+
+ def skip_project_check?
+ !for_project_noteable?
+ end
+
+ def for_epic?
+ noteable.class.name == 'Epic'
+ end
+
+ def user_mention_resource_id
+ noteable_id || commit_id
+ end
+
+ def user_mention_note_id
+ id
+ end
+
+ private
+
+ def mentionable_params
+ return super unless for_epic?
+
+ super.merge(banzai_context_params)
+ end
+
+ def banzai_context_params
+ { group: noteable.group, label_url_method: :group_epics_url }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/batch_worker_context.rb b/lib/gitlab/batch_worker_context.rb
new file mode 100644
index 00000000000..0589206fefc
--- /dev/null
+++ b/lib/gitlab/batch_worker_context.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class BatchWorkerContext
+ def initialize(objects, arguments_proc:, context_proc:)
+ @objects = objects
+ @arguments_proc = arguments_proc
+ @context_proc = context_proc
+ end
+
+ def arguments
+ context_by_arguments.keys
+ end
+
+ def context_for(arguments)
+ context_by_arguments[arguments]
+ end
+
+ private
+
+ attr_reader :objects, :arguments_proc, :context_proc
+
+ def context_by_arguments
+ @context_by_arguments ||= objects.each_with_object({}) do |object, result|
+ arguments = Array.wrap(arguments_proc.call(object))
+ context = Gitlab::ApplicationContext.new(context_proc.call(object))
+
+ result[arguments] = context
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index ea7013db2ce..e7a7d23ef7e 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -59,6 +59,10 @@ module Gitlab
end
self.loaded = true
+ rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e
+ # Handle Gitaly connection issues gracefully
+ Gitlab::ErrorTracking
+ .track_exception(e, project_id: project.id)
end
def load_from_project
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index a737d5543ad..3a05feee156 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -15,7 +15,7 @@ module Gitlab
4 => 'blue',
5 => 'magenta',
6 => 'cyan',
- 7 => 'white', # not that this is gray in the dark (aka default) color table
+ 7 => 'white' # not that this is gray in the dark (aka default) color table
}.freeze
STYLE_SWITCHES = {
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index c705b6f86c7..a500a0cc35d 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -6,11 +6,12 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
- Result = Struct.new(:when, :start_in) do
+ Result = Struct.new(:when, :start_in, :allow_failure) do
def build_attributes
{
when: self.when,
- options: { start_in: start_in }.compact
+ options: { start_in: start_in }.compact,
+ allow_failure: allow_failure
}.compact
end
@@ -30,7 +31,8 @@ module Gitlab
elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
- matched_rule.attributes[:start_in]
+ matched_rule.attributes[:start_in],
+ matched_rule.attributes[:allow_failure]
)
else
Result.new('never')
diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb
new file mode 100644
index 00000000000..c0247dca73d
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/bridge.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a CI/CD Bridge job that is responsible for
+ # defining a downstream project trigger.
+ #
+ class Bridge < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Inheritable
+
+ ALLOWED_KEYS = %i[trigger stage allow_failure only except
+ when extends variables needs rules].freeze
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, presence: true
+ validates :name, presence: true
+ validates :name, type: Symbol
+ validates :config, disallowed_keys: {
+ in: %i[only except when start_in],
+ message: 'key may not be used with `rules`'
+ },
+ if: :has_rules?
+
+ with_options allow_nil: true do
+ validates :when,
+ inclusion: { in: %w[on_success on_failure always],
+ message: 'should be on_success, on_failure or always' }
+ validates :extends, type: String
+ validates :rules, array_of_hashes: true
+ end
+
+ validate on: :composed do
+ unless trigger.present? || bridge_needs.present?
+ errors.add(:config, 'should contain either a trigger or a needs:pipeline')
+ end
+ end
+
+ validate on: :composed do
+ next unless bridge_needs.present?
+ next if bridge_needs.one?
+
+ errors.add(:config, 'should contain at most one bridge need')
+ end
+ end
+
+ entry :trigger, ::Gitlab::Ci::Config::Entry::Trigger,
+ description: 'CI/CD Bridge downstream trigger definition.',
+ inherit: false
+
+ entry :needs, ::Gitlab::Ci::Config::Entry::Needs,
+ description: 'CI/CD Bridge needs dependency definition.',
+ inherit: false,
+ metadata: { allowed_needs: %i[job bridge] }
+
+ entry :stage, ::Gitlab::Ci::Config::Entry::Stage,
+ description: 'Pipeline stage this job will be executed into.',
+ inherit: false
+
+ entry :only, ::Gitlab::Ci::Config::Entry::Policy,
+ description: 'Refs policy this job will be executed for.',
+ default: ::Gitlab::Ci::Config::Entry::Policy::DEFAULT_ONLY,
+ inherit: false
+
+ entry :except, ::Gitlab::Ci::Config::Entry::Policy,
+ description: 'Refs policy this job will be executed for.',
+ inherit: false
+
+ entry :rules, ::Gitlab::Ci::Config::Entry::Rules,
+ description: 'List of evaluable Rules to determine job inclusion.',
+ inherit: false,
+ metadata: {
+ allowed_when: %w[on_success on_failure always never manual delayed].freeze
+ }
+
+ entry :variables, ::Gitlab::Ci::Config::Entry::Variables,
+ description: 'Environment variables available for this job.',
+ inherit: false
+
+ helpers(*ALLOWED_KEYS)
+ attributes(*ALLOWED_KEYS)
+
+ def self.matching?(name, config)
+ !name.to_s.start_with?('.') &&
+ config.is_a?(Hash) &&
+ (config.key?(:trigger) || config.key?(:needs))
+ end
+
+ def self.visible?
+ true
+ end
+
+ def compose!(deps = nil)
+ super do
+ has_workflow_rules = deps&.workflow&.has_rules?
+
+ # If workflow:rules: or rules: are used
+ # they are considered not compatible
+ # with `only/except` defaults
+ #
+ # Context: https://gitlab.com/gitlab-org/gitlab/merge_requests/21742
+ if has_rules? || has_workflow_rules
+ # Remove only/except defaults
+ # defaults are not considered as defined
+ @entries.delete(:only) unless only_defined?
+ @entries.delete(:except) unless except_defined?
+ end
+ end
+ end
+
+ def has_rules?
+ @config&.key?(:rules)
+ end
+
+ def name
+ @metadata[:name]
+ end
+
+ def value
+ { name: name,
+ trigger: (trigger_value if trigger_defined?),
+ needs: (needs_value if needs_defined?),
+ ignore: !!allow_failure,
+ stage: stage_value,
+ when: when_value,
+ extends: extends_value,
+ variables: (variables_value if variables_defined?),
+ rules: (rules_value if has_rules?),
+ only: only_value,
+ except: except_value,
+ scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage }.compact
+ end
+
+ def bridge_needs
+ needs_value[:bridge] if needs_value
+ end
+
+ private
+
+ def overwrite_entry(deps, key, current_entry)
+ deps.default[key] unless current_entry.specified?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 124581c961f..ffc8cb887e8 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -258,7 +258,8 @@ module Gitlab
after_script: after_script_value,
ignore: ignored?,
needs: needs_defined? ? needs_value : nil,
- resource_group: resource_group }
+ resource_group: resource_group,
+ scheduling_type: needs_defined? ? :dag : :stage }
end
end
end
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index b517dae4d2e..1d3036189b0 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -36,7 +36,7 @@ module Gitlab
end
end
- TYPES = [Entry::Hidden, Entry::Job].freeze
+ TYPES = [Entry::Hidden, Entry::Job, Entry::Bridge].freeze
private_constant :TYPES
@@ -77,5 +77,3 @@ module Gitlab
end
end
end
-
-::Gitlab::Ci::Config::Entry::Jobs.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Jobs')
diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb
index 5301c453ed4..d7ba8624882 100644
--- a/lib/gitlab/ci/config/entry/needs.rb
+++ b/lib/gitlab/ci/config/entry/needs.rb
@@ -11,12 +11,14 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, presence: true
-
validate do
unless config.is_a?(Hash) || config.is_a?(Array)
errors.add(:config, 'can only be a Hash or an Array')
end
+
+ if config.is_a?(Hash) && config.empty?
+ errors.add(:config, 'can not be an empty Hash')
+ end
end
validate on: :composed do
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index f984d7d397a..571e056e096 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -11,7 +11,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics].freeze
+ ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics lsif].freeze
attributes ALLOWED_KEYS
@@ -30,6 +30,7 @@ module Gitlab
validates :license_management, array_of_strings_or_string: true
validates :license_scanning, array_of_strings_or_string: true
validates :metrics, array_of_strings_or_string: true
+ validates :lsif, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 59e0ef583ae..8ffd49b8a93 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -9,10 +9,10 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
CLAUSES = %i[if changes exists].freeze
- ALLOWED_KEYS = %i[if changes exists when start_in].freeze
+ ALLOWED_KEYS = %i[if changes exists when start_in allow_failure].freeze
ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
- attributes :if, :changes, :exists, :when, :start_in
+ attributes :if, :changes, :exists, :when, :start_in, :allow_failure
validations do
validates :config, presence: true
@@ -26,6 +26,7 @@ module Gitlab
validates :if, expression: true
validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
validates :when, allowed_values: { in: ALLOWABLE_WHEN }
+ validates :allow_failure, boolean: true
end
validate do
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
new file mode 100644
index 00000000000..7202784842a
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a cross-project downstream trigger.
+ #
+ class Trigger < ::Gitlab::Config::Entry::Simplifiable
+ strategy :SimpleTrigger, if: -> (config) { config.is_a?(String) }
+ strategy :ComplexTrigger, if: -> (config) { config.is_a?(Hash) }
+
+ class SimpleTrigger < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations { validates :config, presence: true }
+
+ def value
+ { project: @config }
+ end
+ end
+
+ class ComplexTrigger < ::Gitlab::Config::Entry::Simplifiable
+ strategy :CrossProjectTrigger, if: -> (config) { !config.key?(:include) }
+
+ strategy :SameProjectTrigger, if: -> (config) do
+ ::Feature.enabled?(:ci_parent_child_pipeline, default_enabled: true) &&
+ config.key?(:include)
+ end
+
+ class CrossProjectTrigger < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ ALLOWED_KEYS = %i[project branch strategy].freeze
+ attributes :project, :branch, :strategy
+
+ validations do
+ validates :config, presence: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :project, presence: true
+ validates :branch, type: String, allow_nil: true
+ validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true
+ end
+ end
+
+ class SameProjectTrigger < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Configurable
+
+ INCLUDE_MAX_SIZE = 3
+ ALLOWED_KEYS = %i[strategy include].freeze
+ attributes :strategy
+
+ validations do
+ validates :config, presence: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true
+ end
+
+ entry :include, ::Gitlab::Ci::Config::Entry::Includes,
+ description: 'List of external YAML files to include.',
+ reserved: true,
+ metadata: { max_size: INCLUDE_MAX_SIZE }
+
+ def value
+ @config
+ end
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def errors
+ if ::Feature.enabled?(:ci_parent_child_pipeline, default_enabled: true)
+ ['config must specify either project or include']
+ else
+ ['config must specify project']
+ end
+ end
+ end
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def errors
+ ["#{location} has to be either a string or a hash"]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/external/file/template.rb b/lib/gitlab/ci/config/external/file/template.rb
index db56f6a9b00..c4b4a7a0a73 100644
--- a/lib/gitlab/ci/config/external/file/template.rb
+++ b/lib/gitlab/ci/config/external/file/template.rb
@@ -33,7 +33,7 @@ module Gitlab
def template_name
return unless template_name_valid?
- location.first(-SUFFIX.length)
+ location.delete_suffix(SUFFIX)
end
def template_name_valid?
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 8f8cae0b5f2..133eb16a83e 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -50,10 +50,7 @@ module Gitlab
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure']
elsif data['error']
- # For now, as an MVC, we are grouping error test cases together
- # with failed ones. But we will improve this further on
- # https://gitlab.com/gitlab-org/gitlab/issues/32046.
- status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
+ status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR
system_output = data['error']
else
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb
index 54be789988c..8d19a73dfc3 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb
@@ -31,9 +31,7 @@ module Gitlab
end
def beta_enabled?
- Feature.enabled?(:auto_devops_beta, project, default_enabled: true) &&
- # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml`
- Feature.enabled?(:workflow_rules, project, default_enabled: true)
+ Feature.enabled?(:auto_devops_beta, project, default_enabled: true)
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb
index b282886a56f..c72b5f18424 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb
@@ -31,9 +31,7 @@ module Gitlab
end
def beta_enabled?
- Feature.enabled?(:auto_devops_beta, project, default_enabled: true) &&
- # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml`
- Feature.enabled?(:workflow_rules, project, default_enabled: true)
+ Feature.enabled?(:auto_devops_beta, project, default_enabled: true)
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
index 81f5733b279..a793ae9cc24 100644
--- a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
+++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
@@ -9,17 +9,7 @@ module Gitlab
include Chain::Helpers
def perform!
- unless feature_enabled?
- if has_workflow_rules?
- error("Workflow rules are disabled", config_error: true)
- end
-
- return
- end
-
- unless workflow_passed?
- error('Pipeline filtered out by workflow rules.')
- end
+ error('Pipeline filtered out by workflow rules.') unless workflow_passed?
end
def break?
@@ -28,10 +18,6 @@ module Gitlab
private
- def feature_enabled?
- Feature.enabled?(:workflow_rules, @pipeline.project, default_enabled: true)
- end
-
def workflow_passed?
strong_memoize(:workflow_passed) do
workflow_rules.evaluate(@pipeline, global_context).pass?
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 3a40c7b167c..f9ae37aa273 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -17,7 +17,7 @@ module Gitlab
#
pipeline.stages = @command.stage_seeds.map(&:to_resource)
- if pipeline.stages.none?
+ if stage_names.empty?
return error('No stages / jobs for this pipeline.')
end
@@ -31,6 +31,15 @@ module Gitlab
def break?
pipeline.errors.any?
end
+
+ private
+
+ def stage_names
+ # We filter out `.pre/.post` stages, as they alone are not considered
+ # a complete pipeline:
+ # https://gitlab.com/gitlab-org/gitlab/issues/198518
+ pipeline.stages.map(&:name) - ::Gitlab::Ci::Config::EdgeStagesInjector::EDGES
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/deployment.rb b/lib/gitlab/ci/pipeline/seed/deployment.rb
index 8c90f03cb1d..cc63fb4c609 100644
--- a/lib/gitlab/ci/pipeline/seed/deployment.rb
+++ b/lib/gitlab/ci/pipeline/seed/deployment.rb
@@ -24,8 +24,14 @@ module Gitlab
# non-environment job.
return unless deployment.valid? && deployment.environment.persisted?
- deployment.cluster_id =
- deployment.environment.deployment_platform&.cluster_id
+ if cluster_id = deployment.environment.deployment_platform&.cluster_id
+ # double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
+ deployment.cluster_id = cluster_id
+ deployment.deployment_cluster = ::DeploymentCluster.new(
+ cluster_id: cluster_id,
+ kubernetes_namespace: deployment.environment.deployment_namespace
+ )
+ end
# Allocate IID for deployments.
# This operation must be outside of transactions of pipeline creations.
diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb
index 11810bdc0a8..c6f17f0764f 100644
--- a/lib/gitlab/ci/reports/test_reports_comparer.rb
+++ b/lib/gitlab/ci/reports/test_reports_comparer.rb
@@ -29,7 +29,7 @@ module Gitlab
end
end
- %w(total_count resolved_count failed_count).each do |method|
+ %w(total_count resolved_count failed_count error_count).each do |method|
define_method(method) do
# rubocop: disable CodeReuse/ActiveRecord
suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend
diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb
index 9cb7db5934c..a58de43e55e 100644
--- a/lib/gitlab/ci/reports/test_suite_comparer.rb
+++ b/lib/gitlab/ci/reports/test_suite_comparer.rb
@@ -38,6 +38,30 @@ module Gitlab
end
end
+ def new_errors
+ strong_memoize(:new_errors) do
+ head_suite.error.reject do |key, _|
+ base_suite.error.include?(key)
+ end.values
+ end
+ end
+
+ def existing_errors
+ strong_memoize(:existing_errors) do
+ head_suite.error.select do |key, _|
+ base_suite.error.include?(key)
+ end.values
+ end
+ end
+
+ def resolved_errors
+ strong_memoize(:resolved_errors) do
+ head_suite.success.select do |key, _|
+ base_suite.error.include?(key)
+ end.values
+ end
+ end
+
def total_count
head_suite.total_count
end
@@ -47,12 +71,16 @@ module Gitlab
end
def resolved_count
- resolved_failures.count
+ resolved_failures.count + resolved_errors.count
end
def failed_count
new_failures.count + existing_failures.count
end
+
+ def error_count
+ new_errors.count + existing_errors.count
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 910d93f54ce..b0b01538a30 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -18,7 +18,13 @@ module Gitlab
archived_failure: 'archived failure',
unmet_prerequisites: 'unmet prerequisites',
scheduler_failure: 'scheduler failure',
- data_integrity_failure: 'data integrity failure'
+ data_integrity_failure: 'data integrity failure',
+ forward_deployment_failure: 'forward deployment failure',
+ invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
+ downstream_bridge_project_not_found: 'downstream project could not be found',
+ insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
+ bridge_pipeline_is_child_pipeline: 'creation of child pipeline not allowed from another child pipeline',
+ downstream_pipeline_creation_failed: 'downstream pipeline can not be created'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
index 426f0238f9d..c3ca44eea9e 100644
--- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -1,11 +1,13 @@
performance:
stage: performance
- image: docker:stable
+ # pin to a version matching the dind service, just to be safe
+ image: docker:19.03.5
allow_failure: true
variables:
DOCKER_TLS_CERTDIR: ""
services:
- - docker:stable-dind
+ # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed
+ - docker:19.03.5-dind
script:
- |
if ! docker info &>/dev/null; then
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 8061da968ed..488945ffa3e 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -4,7 +4,8 @@ build:
variables:
DOCKER_TLS_CERTDIR: ""
services:
- - docker:stable-dind
+ # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed
+ - docker:19.03.5-dind
script:
- |
if [[ -z "$CI_COMMIT_TAG" ]]; then
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 8bc60a36ebd..dd5144e28a7 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -1,9 +1,11 @@
code_quality:
stage: test
- image: docker:stable
+ # pin to a version matching the dind service, just to be safe
+ image: docker:19.03.5
allow_failure: true
services:
- - docker:stable-dind
+ # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed
+ - docker:19.03.5-dind
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index feedb0994c2..78ee9b28605 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.dast-auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.3"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.1"
dast_environment_deploy:
extends: .dast-auto-deploy
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index d20d04425f6..47cc6caa192 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.3"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.3"
review:
extends: .auto-deploy
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
index b9fe838d1da..a0ddd273552 100644
--- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -1,6 +1,6 @@
test:
services:
- - postgres:latest
+ - "postgres:${POSTGRES_VERSION}"
variables:
POSTGRES_DB: test
stage: test
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
index 93c69772b01..73ae63c3092 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -1,6 +1,6 @@
apply:
stage: deploy
- image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.5.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.8.0"
environment:
name: production
variables:
@@ -10,6 +10,10 @@ apply:
CERT_MANAGER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cert-manager/values.yaml
SENTRY_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/sentry/values.yaml
GITLAB_RUNNER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/gitlab-runner/values.yaml
+ CILIUM_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cilium/values.yaml
+ JUPYTERHUB_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/jupyterhub/values.yaml
+ PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml
+ ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml
script:
- gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml
only:
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index 225fb7e5606..5ff6413898f 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -57,6 +57,8 @@ dependency_scanning:
PIP_REQUIREMENTS_FILE \
MAVEN_CLI_OPTS \
BUNDLER_AUDIT_UPDATE_DISABLED \
+ BUNDLER_AUDIT_ADVISORY_DB_URL \
+ BUNDLER_AUDIT_ADVISORY_DB_REF_NAME \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
diff --git a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
index f10a445f7c9..58fd018a82d 100644
--- a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
@@ -1,8 +1,5 @@
-# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/license_management/
-#
-# Configure the scanning tool through the environment variables.
-# List of the variables: https://gitlab.com/gitlab-org/security-products/license-management#settings
-# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
+# Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/14624
+# Please, use License-Scanning.gitlab-ci.yml template instead
variables:
LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
@@ -16,6 +13,7 @@ license_management:
SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD
allow_failure: true
script:
+ - echo "This template is deprecated, please use License-Scanning.gitlab-ci.yml template instead."
- /run.sh analyze .
artifacts:
reports:
diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
new file mode 100644
index 00000000000..2333fb4e947
--- /dev/null
+++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
@@ -0,0 +1,33 @@
+# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/license_compliance/
+#
+# Configure the scanning tool through the environment variables.
+# List of the variables: https://gitlab.com/gitlab-org/security-products/license-management#settings
+# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
+
+variables:
+ LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
+
+license_scanning:
+ stage: test
+ image:
+ name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ entrypoint: [""]
+ variables:
+ SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD
+ allow_failure: true
+ script:
+ - /run.sh analyze .
+ after_script:
+ - mv gl-license-management-report.json gl-license-scanning-report.json
+ artifacts:
+ reports:
+ license_scanning: gl-license-scanning-report.json
+ dependencies: []
+ only:
+ refs:
+ - branches
+ variables:
+ - $GITLAB_FEATURES =~ /\blicense_scanning\b/
+ except:
+ variables:
+ - $LICENSE_MANAGEMENT_DISABLED
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 864e3eb569d..51a1f4e549b 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -37,11 +37,8 @@ sast:
fi
fi
- |
- printenv | grep -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | cut -d'=' -f1 | \
- (while IFS='\\n' read -r VAR; do unset -v "$VAR"; done; /bin/printenv > .env)
- - |
- docker run \
- --env-file .env \
+ ENVS=`printenv | grep -vE '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '`
+ docker run "$ENVS" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 080a8ac107d..ae3ff4a51e2 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -65,6 +65,7 @@ module Gitlab
rules: job[:rules],
cache: job[:cache],
resource_group_key: job[:resource_group],
+ scheduling_type: job[:scheduling_type],
options: {
image: job[:image],
services: job[:services],
diff --git a/lib/gitlab/color_schemes.rb b/lib/gitlab/color_schemes.rb
index 881e5dbc923..620b4a8aee6 100644
--- a/lib/gitlab/color_schemes.rb
+++ b/lib/gitlab/color_schemes.rb
@@ -66,5 +66,9 @@ module Gitlab
default
end
end
+
+ def self.valid_ids
+ SCHEMES.map(&:id)
+ end
end
end
diff --git a/lib/gitlab/content_disposition.rb b/lib/gitlab/content_disposition.rb
deleted file mode 100644
index ff6154a5b26..00000000000
--- a/lib/gitlab/content_disposition.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-# This ports ActionDispatch::Http::ContentDisposition (https://github.com/rails/rails/pull/33829,
-# which will be available in Rails 6.
-module Gitlab
- class ContentDisposition # :nodoc:
- # Make sure we remove this patch starting with Rails 6.0.
- if Rails.version.start_with?('6.0')
- raise <<~MSG
- Please remove this file and use `ActionDispatch::Http::ContentDisposition` instead.
- MSG
- end
-
- def self.format(disposition:, filename:)
- new(disposition: disposition, filename: filename).to_s
- end
-
- attr_reader :disposition, :filename
-
- def initialize(disposition:, filename:)
- @disposition = disposition
- @filename = filename
- end
-
- # rubocop:disable Style/VariableInterpolation
- TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/.freeze
-
- def ascii_filename
- 'filename="' + percent_escape(::I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
- end
-
- RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/.freeze
- # rubocop:enable Style/VariableInterpolation
-
- def utf8_filename
- "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
- end
-
- def to_s
- if filename
- "#{disposition}; #{ascii_filename}; #{utf8_filename}"
- else
- "#{disposition}"
- end
- end
-
- private
-
- def percent_escape(string, pattern)
- string.gsub(pattern) do |char|
- char.bytes.map { |byte| "%%%02X" % byte }.join
- end
- end
- end
-end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 6ce47650562..891fd8c1bb5 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -50,7 +50,7 @@ module Gitlab
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
- if ActiveRecord::Base.connection.migration_context.needs_migration?
+ if Gitlab::Runtime.rake? && ActiveRecord::Base.connection.migration_context.needs_migration?
db_attributes = current_settings&.attributes || {}
fake_application_settings(db_attributes)
elsif current_settings.present?
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index 1cd54238bb4..06f0cbed147 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -50,7 +50,7 @@ module Gitlab
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
+ # value stream analytics stage.
median_datetime(cte_table, interval_query(project_ids), name)
end
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
index 644300caead..acfb641aeec 100644
--- a/lib/gitlab/cycle_analytics/usage_data.rb
+++ b/lib/gitlab/cycle_analytics/usage_data.rb
@@ -12,7 +12,7 @@ module Gitlab
@options = { from: 7.days.ago }
end
- def to_json
+ def to_json(*)
total = 0
values =
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index f7b7db50b2f..e6702c5a38b 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -38,11 +38,7 @@ module Gitlab
project_id: project.id,
project_name: project.full_name,
- user: {
- id: user.try(:id),
- name: user.try(:name),
- email: user.try(:email)
- },
+ user: user.try(:hook_attrs),
commit: {
# note: commit.id is actually the pipeline id
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 65cfd47e1e8..41ceeb329b3 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -37,7 +37,7 @@ module Gitlab
id: "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
message: "Add simple search to projects in public area",
timestamp: "2013-05-13T18:18:08+00:00",
- url: "https://test.example.com/gitlab/gitlab/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ url: "https://test.example.com/gitlab/gitlab/-/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
author: {
name: "Test User",
email: "test@example.com"
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 82ec740ade1..02005be1f6a 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -204,15 +204,16 @@ module Gitlab
# pool_size - The size of the DB pool.
# host - An optional host name to use instead of the default one.
def self.create_connection_pool(pool_size, host = nil, port = nil)
- # See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb
env = Rails.env
- original_config = ActiveRecord::Base.configurations
+ original_config = ActiveRecord::Base.configurations.to_h
env_config = original_config[env].merge('pool' => pool_size)
env_config['host'] = host if host
env_config['port'] = port if port
- config = original_config.merge(env => env_config)
+ config = ActiveRecord::DatabaseConfigurations.new(
+ original_config.merge(env => env_config)
+ )
spec =
ActiveRecord::
@@ -232,7 +233,7 @@ module Gitlab
end
def self.cached_table_exists?(table_name)
- connection.schema_cache.data_source_exists?(table_name)
+ exists? && connection.schema_cache.data_source_exists?(table_name)
end
def self.database_version
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
new file mode 100644
index 00000000000..a9d4665bc5f
--- /dev/null
+++ b/lib/gitlab/database/batch_count.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
+# Implements a distinct and ordinary batch counter
+# Needs indexes on the column below to calculate max, min and range queries
+# For larger tables just set use higher batch_size with index optimization
+# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705
+# Examples:
+# extend ::Gitlab::Database::BatchCount
+# batch_count(User.active)
+# batch_count(::Clusters::Cluster.aws_installed.enabled, :cluster_id)
+# batch_distinct_count(::Project, :creator_id)
+module Gitlab
+ module Database
+ module BatchCount
+ def batch_count(relation, column = nil, batch_size: nil)
+ BatchCounter.new(relation, column: column).count(batch_size: batch_size)
+ end
+
+ def batch_distinct_count(relation, column = nil, batch_size: nil)
+ BatchCounter.new(relation, column: column).count(mode: :distinct, batch_size: batch_size)
+ end
+
+ class << self
+ include BatchCount
+ end
+ end
+
+ class BatchCounter
+ FALLBACK = -1
+ MIN_REQUIRED_BATCH_SIZE = 2_000
+ MAX_ALLOWED_LOOPS = 10_000
+ SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep
+ # Each query should take <<500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705
+ DEFAULT_DISTINCT_BATCH_SIZE = 100_000
+ DEFAULT_BATCH_SIZE = 10_000
+
+ def initialize(relation, column: nil)
+ @relation = relation
+ @column = column || relation.primary_key
+ end
+
+ def unwanted_configuration?(finish, batch_size, start)
+ batch_size <= MIN_REQUIRED_BATCH_SIZE ||
+ (finish - start) / batch_size >= MAX_ALLOWED_LOOPS ||
+ start > finish
+ end
+
+ def count(batch_size: nil, mode: :itself)
+ raise 'BatchCount can not be run inside a transaction' if ActiveRecord::Base.connection.transaction_open?
+ raise "The mode #{mode.inspect} is not supported" unless [:itself, :distinct].include?(mode)
+
+ # non-distinct have better performance
+ batch_size ||= mode == :distinct ? DEFAULT_BATCH_SIZE : DEFAULT_DISTINCT_BATCH_SIZE
+
+ start = @relation.minimum(@column) || 0
+ finish = @relation.maximum(@column) || 0
+
+ raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0
+ return FALLBACK if unwanted_configuration?(finish, batch_size, start)
+
+ counter = 0
+ batch_start = start
+
+ while batch_start <= finish
+ begin
+ counter += batch_fetch(batch_start, batch_start + batch_size, mode)
+ batch_start += batch_size
+ rescue ActiveRecord::QueryCanceled
+ # retry with a safe batch size & warmer cache
+ if batch_size >= 2 * MIN_REQUIRED_BATCH_SIZE
+ batch_size /= 2
+ else
+ return FALLBACK
+ end
+ end
+ sleep(SLEEP_TIME_IN_SECONDS)
+ end
+
+ counter
+ end
+
+ def batch_fetch(start, finish, mode)
+ # rubocop:disable GitlabSecurity/PublicSend
+ @relation.select(@column).public_send(mode).where(@column => start..(finish - 1)).count
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index b7d510c19f9..3b6684b861c 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -280,6 +280,46 @@ module Gitlab
end
end
+ # Executes the block with a retry mechanism that alters the +lock_timeout+ and +sleep_time+ between attempts.
+ # The timings can be controlled via the +timing_configuration+ parameter.
+ # If the lock was not acquired within the retry period, a last attempt is made without using +lock_timeout+.
+ #
+ # ==== Examples
+ # # Invoking without parameters
+ # with_lock_retries do
+ # drop_table :my_table
+ # end
+ #
+ # # Invoking with custom +timing_configuration+
+ # t = [
+ # [1.second, 1.second],
+ # [2.seconds, 2.seconds]
+ # ]
+ #
+ # with_lock_retries(timing_configuration: t) do
+ # drop_table :my_table # this will be retried twice
+ # end
+ #
+ # # Disabling the retries using an environment variable
+ # > export DISABLE_LOCK_RETRIES=true
+ #
+ # with_lock_retries do
+ # drop_table :my_table # one invocation, it will not retry at all
+ # end
+ #
+ # ==== Parameters
+ # * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION`
+ # * +logger+ - [Gitlab::JsonLogger]
+ # * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES`
+ def with_lock_retries(**args, &block)
+ merged_args = {
+ klass: self.class,
+ logger: Gitlab::BackgroundMigration::Logger
+ }.merge(args)
+
+ Gitlab::Database::WithLockRetries.new(merged_args).run(&block)
+ end
+
def true_value
Database.true_value
end
@@ -342,7 +382,7 @@ module Gitlab
count_arel = table.project(Arel.star.count.as('count'))
count_arel = yield table, count_arel if block_given?
- total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i
+ total = exec_query(count_arel.to_sql).to_a.first['count'].to_i
return if total == 0
@@ -359,7 +399,7 @@ module Gitlab
start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
start_arel = yield table, start_arel if block_given?
- start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i
+ start_id = exec_query(start_arel.to_sql).to_a.first['id'].to_i
loop do
stop_arel = table.project(table[:id])
@@ -369,7 +409,7 @@ module Gitlab
.skip(batch_size)
stop_arel = yield table, stop_arel if block_given?
- stop_row = exec_query(stop_arel.to_sql).to_hash.first
+ stop_row = exec_query(stop_arel.to_sql).to_a.first
update_arel = Arel::UpdateManager.new
.table(table)
@@ -1079,6 +1119,20 @@ into similar problems in the future (e.g. when new tables are created).
SQL
end
+ # Note this should only be used with very small tables
+ def backfill_iids(table)
+ sql = <<-END
+ UPDATE #{table}
+ SET iid = #{table}_with_calculated_iid.iid_num
+ FROM (
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY id ASC) AS iid_num FROM #{table}
+ ) AS #{table}_with_calculated_iid
+ WHERE #{table}.id = #{table}_with_calculated_iid.id
+ END
+
+ execute(sql)
+ end
+
private
def tables_match?(target_table, foreign_key_table)
diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb
index 776e80701f1..bad6d3e2a9b 100644
--- a/lib/gitlab/database/sha_attribute.rb
+++ b/lib/gitlab/database/sha_attribute.rb
@@ -24,7 +24,14 @@ module Gitlab
def serialize(value)
arg = value ? [value].pack(PACK_FORMAT) : nil
- super(arg)
+ BINARY_TYPE.new.serialize(arg)
+ end
+
+ # Casts a SHA1 in hexadecimal to the proper binary format.
+ def self.serialize(value)
+ arg = value ? [value].pack(PACK_FORMAT) : nil
+
+ BINARY_TYPE.new.serialize(arg)
end
end
end
diff --git a/lib/gitlab/database/subquery.rb b/lib/gitlab/database/subquery.rb
deleted file mode 100644
index 2a6f39c6a27..00000000000
--- a/lib/gitlab/database/subquery.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Database
- module Subquery
- class << self
- def self_join(relation)
- t = relation.arel_table
- t2 = relation.arel.as('t2')
-
- relation.unscoped.joins(t.join(t2).on(t[:id].eq(t2[:id])).join_sources.first)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
new file mode 100644
index 00000000000..2f36bfa1480
--- /dev/null
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class WithLockRetries
+ NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
+
+ # Each element of the array represents a retry iteration.
+ # - DEFAULT_TIMING_CONFIGURATION.size provides the iteration count.
+ # - First element: DB lock_timeout
+ # - Second element: Sleep time after unsuccessful lock attempt (LockWaitTimeout error raised)
+ # - Worst case, this configuration would retry for about 40 minutes.
+ DEFAULT_TIMING_CONFIGURATION = [
+ [0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms
+ [0.1.seconds, 0.05.seconds],
+ [0.2.seconds, 0.05.seconds],
+ [0.3.seconds, 0.10.seconds],
+ [0.4.seconds, 0.15.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [1.second, 5.seconds], # probably high traffic, increase timings
+ [1.second, 1.minute],
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.2.seconds, 0.05.seconds],
+ [0.3.seconds, 0.10.seconds],
+ [0.4.seconds, 0.15.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [3.seconds, 3.minutes], # probably high traffic or long locks, increase timings
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [5.seconds, 2.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [10.seconds, 10.minutes],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [10.seconds, 10.minutes]
+ ].freeze
+
+ def initialize(logger: NULL_LOGGER, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV)
+ @logger = logger
+ @klass = klass
+ @timing_configuration = timing_configuration
+ @env = env
+ @current_iteration = 1
+ @log_params = { method: 'with_lock_retries', class: klass.to_s }
+ end
+
+ def run(&block)
+ raise 'no block given' unless block_given?
+
+ @block = block
+
+ if lock_retries_disabled?
+ log(message: 'DISABLE_LOCK_RETRIES environment variable is true, executing the block without retry')
+
+ return run_block
+ end
+
+ begin
+ run_block_with_transaction
+ rescue ActiveRecord::LockWaitTimeout
+ if retry_with_lock_timeout?
+ wait_until_next_retry
+
+ retry
+ else
+ run_block_without_lock_timeout
+ end
+ end
+ end
+
+ private
+
+ attr_reader :logger, :env, :block, :current_iteration, :log_params, :timing_configuration
+
+ def run_block
+ block.call
+ end
+
+ def run_block_with_transaction
+ ActiveRecord::Base.transaction(requires_new: true) do
+ execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
+
+ log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+ end
+ end
+
+ def retry_with_lock_timeout?
+ current_iteration != retry_count
+ end
+
+ def wait_until_next_retry
+ log(message: 'ActiveRecord::LockWaitTimeout error, retrying after sleep', current_iteration: current_iteration, sleep_time_in_seconds: current_sleep_time_in_seconds)
+
+ sleep(current_sleep_time_in_seconds)
+
+ @current_iteration += 1
+ end
+
+ def run_block_without_lock_timeout
+ log(message: "Couldn't acquire lock to perform the migration", current_iteration: current_iteration)
+ log(message: "Executing the migration without lock timeout", current_iteration: current_iteration)
+
+ execute("SET LOCAL lock_timeout TO '0'")
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration)
+ end
+
+ def lock_retries_disabled?
+ Gitlab::Utils.to_boolean(env['DISABLE_LOCK_RETRIES'])
+ end
+
+ def log(params)
+ logger.info(log_params.merge(params))
+ end
+
+ def execute(statement)
+ ActiveRecord::Base.connection.execute(statement)
+ end
+
+ def retry_count
+ timing_configuration.size
+ end
+
+ def current_lock_timeout_in_ms
+ Integer(timing_configuration[current_iteration - 1][0].in_milliseconds)
+ end
+
+ def current_sleep_time_in_seconds
+ timing_configuration[current_iteration - 1][1].to_f
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/x509_serial_number_attribute.rb b/lib/gitlab/database/x509_serial_number_attribute.rb
new file mode 100644
index 00000000000..e12f64787e7
--- /dev/null
+++ b/lib/gitlab/database/x509_serial_number_attribute.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Class for casting binary data to int.
+ #
+ # Using X509SerialNumberAttribute allows you to store X509 certificate
+ # serial number values as binary while still using integer to access them.
+ # rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum), could be:
+ # - 1461501637330902918203684832716283019655932542975
+ # - 0xffffffffffffffffffffffffffffffffffffffff
+ class X509SerialNumberAttribute < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea
+ PACK_FORMAT = 'H*'
+
+ def deserialize(value)
+ value = super(value)
+ value ? value.unpack1(PACK_FORMAT).to_i : nil
+ end
+
+ def serialize(value)
+ arg = value ? [value.to_s].pack(PACK_FORMAT) : nil
+ super(arg)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database_importers/common_metrics.rb b/lib/gitlab/database_importers/common_metrics.rb
index b9d320f2fc7..f964ae8a275 100644
--- a/lib/gitlab/database_importers/common_metrics.rb
+++ b/lib/gitlab/database_importers/common_metrics.rb
@@ -6,5 +6,3 @@ module Gitlab
end
end
end
-
-Gitlab::DatabaseImporters::CommonMetrics.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics')
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
index 409a1252da1..fb0fcc5a93b 100644
--- a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
+++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
@@ -17,7 +17,9 @@ module Gitlab
# custom groups
business: 0,
response: 1,
- system: 2
+ system: 2,
+
+ cluster_health: -100
}
end
@@ -31,12 +33,11 @@ module Gitlab
ha_proxy: _('Response metrics (HA Proxy)'),
aws_elb: _('Response metrics (AWS ELB)'),
nginx: _('Response metrics (NGINX)'),
- kubernetes: _('System metrics (Kubernetes)')
+ kubernetes: _('System metrics (Kubernetes)'),
+ cluster_health: _('Cluster Health')
}
end
end
end
end
end
-
-::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums')
diff --git a/lib/gitlab/database_importers/self_monitoring/helpers.rb b/lib/gitlab/database_importers/self_monitoring/helpers.rb
index d7e90967e89..6956401e20d 100644
--- a/lib/gitlab/database_importers/self_monitoring/helpers.rb
+++ b/lib/gitlab/database_importers/self_monitoring/helpers.rb
@@ -13,11 +13,11 @@ module Gitlab
end
def self_monitoring_project
- application_settings.instance_administration_project
+ application_settings.self_monitoring_project
end
def self_monitoring_project_id
- application_settings.instance_administration_project_id
+ application_settings.self_monitoring_project_id
end
end
end
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index d08afeef3b6..07a4c3bf5e6 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -9,12 +9,13 @@ module Gitlab
include SelfMonitoring::Helpers
VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL
- PROJECT_NAME = 'GitLab Instance Administration'
+ PROJECT_NAME = 'GitLab self monitoring'
steps :validate_application_settings,
:create_group,
:create_project,
:save_project_id,
+ :create_environment,
:add_prometheus_manual_configuration,
:track_event
@@ -69,10 +70,19 @@ module Gitlab
return success(result) if project_created?
response = application_settings.update(
- instance_administration_project_id: result[:project].id
+ self_monitoring_project_id: result[:project].id
)
if response
+ # In the add_prometheus_manual_configuration method, the Prometheus
+ # listen_address config is saved as an api_url in the PrometheusService
+ # model. There are validates hooks in the PrometheusService model that
+ # check if the project associated with the PrometheusService is the
+ # self_monitoring project. It checks
+ # Gitlab::CurrentSettings.self_monitoring_project_id, which is why the
+ # Gitlab::CurrentSettings cache needs to be expired here, so that
+ # PrometheusService sees the latest self_monitoring_project_id.
+ Gitlab::CurrentSettings.expire_current_application_settings
success(result)
else
log_error("Could not save instance administration project ID, errors: %{errors}" % { errors: application_settings.errors.full_messages })
@@ -80,6 +90,19 @@ module Gitlab
end
end
+ def create_environment(result)
+ return success(result) if result[:project].environments.exists?
+
+ environment = ::Environment.new(project_id: result[:project].id, name: 'production')
+
+ if environment.save
+ success(result)
+ else
+ log_error("Could not create environment for the Self monitoring project. Errors: %{errors}" % { errors: environment.errors.full_messages })
+ error(_('Could not create environment'))
+ end
+ end
+
def add_prometheus_manual_configuration(result)
return success(result) unless prometheus_enabled?
return success(result) unless prometheus_listen_address.present?
@@ -115,7 +138,7 @@ module Gitlab
def docs_path
Rails.application.routes.url_helpers.help_page_path(
- 'administration/monitoring/gitlab_instance_administration_project/index'
+ 'administration/monitoring/gitlab_self_monitoring_project/index'
)
end
diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb
index d24c137793e..9166e9091ac 100644
--- a/lib/gitlab/dependency_linker/godeps_json_linker.rb
+++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb
@@ -12,10 +12,12 @@ module Gitlab
def link_dependencies
link_json('ImportPath') do |path|
case path
+ when %r{\A(?<repo>github\.com/#{REPO_REGEX})/(?<path>.+)\z}
+ "https://#{$~[:repo]}/tree/master/#{$~[:path]}"
when %r{\A(?<repo>gitlab\.com/#{NESTED_REPO_REGEX})\.git/(?<path>.+)\z},
- %r{\A(?<repo>git(lab|hub)\.com/#{REPO_REGEX})/(?<path>.+)\z}
+ %r{\A(?<repo>gitlab\.com/#{REPO_REGEX})/(?<path>.+)\z}
- "https://#{$~[:repo]}/tree/master/#{$~[:path]}"
+ "https://#{$~[:repo]}/-/tree/master/#{$~[:path]}"
when /\Agolang\.org/
"https://godoc.org/#{path}"
else
diff --git a/lib/gitlab/diff/deprecated_highlight_cache.rb b/lib/gitlab/diff/deprecated_highlight_cache.rb
deleted file mode 100644
index 47347686973..00000000000
--- a/lib/gitlab/diff/deprecated_highlight_cache.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-#
-module Gitlab
- module Diff
- class DeprecatedHighlightCache
- delegate :diffable, to: :@diff_collection
- delegate :diff_options, to: :@diff_collection
-
- def initialize(diff_collection, backend: Rails.cache)
- @backend = backend
- @diff_collection = diff_collection
- end
-
- # - Reads from cache
- # - Assigns DiffFile#highlighted_diff_lines for cached files
- def decorate(diff_file)
- if content = read_file(diff_file)
- diff_file.highlighted_diff_lines = content.map do |line|
- Gitlab::Diff::Line.init_from_hash(line)
- end
- end
- end
-
- # It populates a Hash in order to submit a single write to the memory
- # cache. This avoids excessive IO generated by N+1's (1 writing for
- # each highlighted line or file).
- def write_if_empty
- return if cached_content.present?
-
- @diff_collection.diff_files.each do |diff_file|
- next unless cacheable?(diff_file)
-
- diff_file_id = diff_file.file_identifier
-
- cached_content[diff_file_id] = diff_file.highlighted_diff_lines.map(&:to_hash)
- end
-
- cache.write(key, cached_content, expires_in: 1.week)
- end
-
- def clear
- cache.delete(key)
- end
-
- def key
- [diffable, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options]
- end
-
- private
-
- def read_file(diff_file)
- cached_content[diff_file.file_identifier]
- end
-
- def cache
- @backend
- end
-
- def cached_content
- @cached_content ||= cache.read(key) || {}
- end
-
- def cacheable?(diff_file)
- diffable.present? && diff_file.text? && diff_file.diffable?
- end
- end
- end
-end
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
index dc245377ccc..12b93af3f26 100644
--- a/lib/gitlab/diff/diff_refs.rb
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -37,7 +37,7 @@ module Gitlab
# We have `base_sha` directly available on `DiffRefs` because it's faster#
# than having to look it up in the repo every time.
def complete?
- start_sha && head_sha
+ start_sha.present? && head_sha.present?
end
def compare_in(project)
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 2ba38f31720..4fc5bfddf0c 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -350,6 +350,12 @@ module Gitlab
private
+ def fetch_blob(sha, path)
+ return unless sha
+
+ Blob.lazy(repository.project, sha, path)
+ end
+
def total_blob_lines(blob)
@total_lines ||= begin
line_count = blob.lines.size
@@ -385,15 +391,11 @@ module Gitlab
end
def new_blob_lazy
- return unless new_content_sha
-
- Blob.lazy(repository.project, new_content_sha, file_path)
+ fetch_blob(new_content_sha, file_path)
end
def old_blob_lazy
- return unless old_content_sha
-
- Blob.lazy(repository.project, old_content_sha, old_path)
+ fetch_blob(old_content_sha, old_path)
end
def simple_viewer_class
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index d27da186de0..d126fdb2be2 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -47,11 +47,7 @@ module Gitlab
private
def cache
- @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project, default_enabled: true)
- Gitlab::Diff::HighlightCache.new(self)
- else
- Gitlab::Diff::DeprecatedHighlightCache.new(self)
- end
+ @cache ||= Gitlab::Diff::HighlightCache.new(self)
end
end
end
diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb
index 5bc9f0c337f..d0c13dee1aa 100644
--- a/lib/gitlab/diff/formatters/image_formatter.rb
+++ b/lib/gitlab/diff/formatters/image_formatter.rb
@@ -23,7 +23,7 @@ module Gitlab
end
def complete?
- x && y && width && height
+ [x, y, width, height].all?(&:present?)
end
def to_h
@@ -37,7 +37,9 @@ module Gitlab
def ==(other)
other.is_a?(self.class) &&
x == other.x &&
- y == other.y
+ y == other.y &&
+ width == other.width &&
+ height == other.height
end
end
end
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
index f6e247ef665..5b670b1f83b 100644
--- a/lib/gitlab/diff/formatters/text_formatter.rb
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -19,7 +19,7 @@ module Gitlab
end
def complete?
- old_line || new_line
+ old_line.present? || new_line.present?
end
def to_h
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 403effbb0c6..0a8fbb9a673 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -3,6 +3,7 @@
module Gitlab
module Diff
class HighlightCache
+ include Gitlab::Metrics::Methods
include Gitlab::Utils::StrongMemoize
EXPIRATION = 1.week
@@ -11,6 +12,11 @@ module Gitlab
delegate :diffable, to: :@diff_collection
delegate :diff_options, to: :@diff_collection
+ define_histogram :gitlab_redis_diff_caching_memory_usage_bytes do
+ docstring 'Redis diff caching memory usage by key'
+ buckets [100, 1000, 10000, 100000, 1000000, 10000000]
+ end
+
def initialize(diff_collection)
@diff_collection = diff_collection
end
@@ -57,17 +63,6 @@ module Gitlab
private
- # We create a Gitlab::Diff::DeprecatedHighlightCache here in order to
- # expire deprecated cache entries while we make the transition. This can
- # be removed when :hset_redis_diff_caching is fully launched.
- # See https://gitlab.com/gitlab-org/gitlab/issues/38008
- #
- def deprecated_cache
- strong_memoize(:deprecated_cache) do
- Gitlab::Diff::DeprecatedHighlightCache.new(@diff_collection)
- end
- end
-
def cacheable_files
strong_memoize(:cacheable_files) do
diff_files.select { |file| cacheable?(file) && read_file(file).nil? }
@@ -104,10 +99,6 @@ module Gitlab
#
clear_memoization(:cached_content)
clear_memoization(:cacheable_files)
-
- # Clean up any deprecated hash entries
- #
- deprecated_cache.clear
end
def file_paths
diff --git a/lib/gitlab/diff/suggestion_diff.rb b/lib/gitlab/diff/suggestion_diff.rb
index ee153c226b7..783264fe999 100644
--- a/lib/gitlab/diff/suggestion_diff.rb
+++ b/lib/gitlab/diff/suggestion_diff.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def raw_diff
- "#{diff_header}\n#{from_content_as_diff}#{to_content_as_diff}"
+ "#{diff_header}\n#{from_content_as_diff}\n#{to_content_as_diff}"
end
def diff_header
@@ -26,7 +26,7 @@ module Gitlab
end
def from_content_as_diff
- from_content.lines.map { |line| line.prepend('-') }.join
+ from_content.lines.map { |line| line.prepend('-') }.join.delete_suffix("\n")
end
def to_content_as_diff
diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb
index 0a14a909e31..d8962ec0d20 100644
--- a/lib/gitlab/email/attachment_uploader.rb
+++ b/lib/gitlab/email/attachment_uploader.rb
@@ -12,7 +12,7 @@ module Gitlab
def execute(upload_parent:, uploader_class:)
attachments = []
- message.attachments.each do |attachment|
+ filter_signature_attachments(message).each do |attachment|
tmp = Tempfile.new("gitlab-email-attachment")
begin
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
@@ -32,6 +32,22 @@ module Gitlab
attachments
end
+
+ private
+
+ # If this is a signed message (e.g. S/MIME or PGP), remove the signature
+ # from the uploaded attachments
+ def filter_signature_attachments(message)
+ attachments = message.attachments
+
+ if message.content_type&.starts_with?('multipart/signed')
+ signature_protocol = message.content_type_parameters[:protocol]
+
+ attachments.delete_if { |attachment| attachment.content_type.starts_with?(signature_protocol) } if signature_protocol.present?
+ end
+
+ attachments
+ end
end
end
end
diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb
index e48041d9218..61c9c984f8e 100644
--- a/lib/gitlab/email/hook/smime_signature_interceptor.rb
+++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb
@@ -11,6 +11,7 @@ module Gitlab
cert: certificate.cert,
key: certificate.key,
data: message.encoded)
+
signed_email = Mail.new(signed_message)
overwrite_body(message, signed_email)
diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb
index b331c4ca19c..59d7b0c3c5b 100644
--- a/lib/gitlab/email/smime/certificate.rb
+++ b/lib/gitlab/email/smime/certificate.rb
@@ -4,8 +4,6 @@ module Gitlab
module Email
module Smime
class Certificate
- include OpenSSL
-
attr_reader :key, :cert
def key_string
@@ -17,8 +15,8 @@ module Gitlab
end
def self.from_strings(key_string, cert_string)
- key = PKey::RSA.new(key_string)
- cert = X509::Certificate.new(cert_string)
+ key = OpenSSL::PKey::RSA.new(key_string)
+ cert = OpenSSL::X509::Certificate.new(cert_string)
new(key, cert)
end
diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb
index 2fa83014003..db03e383ecf 100644
--- a/lib/gitlab/email/smime/signer.rb
+++ b/lib/gitlab/email/smime/signer.rb
@@ -7,20 +7,18 @@ module Gitlab
module Smime
# Tooling for signing and verifying data with SMIME
class Signer
- include OpenSSL
-
def self.sign(cert:, key:, data:)
- signed_data = PKCS7.sign(cert, key, data, nil, PKCS7::DETACHED)
- PKCS7.write_smime(signed_data)
+ signed_data = OpenSSL::PKCS7.sign(cert, key, data, nil, OpenSSL::PKCS7::DETACHED)
+ OpenSSL::PKCS7.write_smime(signed_data)
end
# return nil if data cannot be verified, otherwise the signed content data
def self.verify_signature(cert:, ca_cert: nil, signed_data:)
- store = X509::Store.new
+ store = OpenSSL::X509::Store.new
store.set_default_paths
store.add_cert(ca_cert) if ca_cert
- signed_smime = PKCS7.read_smime(signed_data)
+ signed_smime = OpenSSL::PKCS7.read_smime(signed_data)
signed_smime if signed_smime.verify([cert], store)
end
end
diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb
index 6df9bfad657..d20324a613e 100644
--- a/lib/gitlab/error_tracking.rb
+++ b/lib/gitlab/error_tracking.rb
@@ -97,6 +97,8 @@ module Gitlab
extra = extra.merge(data) if data.is_a?(Hash)
end
+ extra = sanitize_request_parameters(extra)
+
if sentry && Raven.configuration.server
Raven.capture_exception(exception, tags: default_tags, extra: extra)
end
@@ -117,6 +119,11 @@ module Gitlab
end
end
+ def sanitize_request_parameters(parameters)
+ filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
+ filter.filter(parameters)
+ end
+
def sentry_dsn
return unless Rails.env.production? || Rails.env.development?
return unless Gitlab.config.sentry.enabled
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
index c240ec1fa4f..b49f2472e01 100644
--- a/lib/gitlab/error_tracking/detailed_error.rb
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -35,7 +35,7 @@ module Gitlab
:user_count
def self.declarative_policy_class
- 'ErrorTracking::DetailedErrorPolicy'
+ 'ErrorTracking::BasePolicy'
end
end
end
diff --git a/lib/gitlab/error_tracking/error.rb b/lib/gitlab/error_tracking/error.rb
index 4af5192aa6a..6bfb9dae610 100644
--- a/lib/gitlab/error_tracking/error.rb
+++ b/lib/gitlab/error_tracking/error.rb
@@ -4,11 +4,16 @@ module Gitlab
module ErrorTracking
class Error
include ActiveModel::Model
+ include GlobalID::Identification
attr_accessor :id, :title, :type, :user_count, :count,
:first_seen, :last_seen, :message, :culprit,
:external_url, :project_id, :project_name, :project_slug,
:short_id, :status, :frequency
+
+ def self.declarative_policy_class
+ 'ErrorTracking::BasePolicy'
+ end
end
end
end
diff --git a/lib/gitlab/error_tracking/error_collection.rb b/lib/gitlab/error_tracking/error_collection.rb
new file mode 100644
index 00000000000..56bcb671363
--- /dev/null
+++ b/lib/gitlab/error_tracking/error_collection.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class ErrorCollection
+ include GlobalID::Identification
+
+ attr_accessor :issues, :external_url, :project
+
+ alias_attribute :gitlab_project, :project
+
+ def initialize(project:, external_url: nil, issues: [])
+ @project = project
+ @external_url = external_url
+ @issues = issues
+ end
+
+ def self.declarative_policy_class
+ 'ErrorTracking::BasePolicy'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb
index c6e0d82f868..015d2c0ead0 100644
--- a/lib/gitlab/error_tracking/error_event.rb
+++ b/lib/gitlab/error_tracking/error_event.rb
@@ -5,7 +5,11 @@ module Gitlab
class ErrorEvent
include ActiveModel::Model
- attr_accessor :issue_id, :date_received, :stack_trace_entries
+ attr_accessor :issue_id, :date_received, :stack_trace_entries, :gitlab_project
+
+ def self.declarative_policy_class
+ 'ErrorTracking::BasePolicy'
+ end
end
end
end
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index a11d6b66409..303e1a23e6b 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -18,7 +18,7 @@ module Gitlab
if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag
- handle_cache_hit(etag, route)
+ handle_cache_hit(etag, route, request)
else
track_cache_miss(if_none_match, cached_value_present, route)
@@ -47,11 +47,13 @@ module Gitlab
%Q{W/"#{value}"}
end
- def handle_cache_hit(etag, route)
+ def handle_cache_hit(etag, route, request)
track_event(:etag_caching_cache_hit, route)
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
+ add_instrument_for_cache_hit(status_code, route, request)
+
[status_code, { 'ETag' => etag, 'X-Gitlab-From-Cache' => 'true' }, []]
end
@@ -68,6 +70,21 @@ module Gitlab
def track_event(name, route)
Gitlab::Metrics.add_event(name, endpoint: route.name)
end
+
+ def add_instrument_for_cache_hit(status, route, request)
+ payload = {
+ etag_route: route.name,
+ params: request.filtered_parameters,
+ headers: request.headers,
+ format: request.format.ref,
+ method: request.request_method,
+ path: request.filtered_path,
+ status: status
+ }
+
+ ActiveSupport::Notifications.instrument(
+ "process_action.action_controller", payload)
+ end
end
end
end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 9d14695c098..7c59267c0b6 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -20,8 +20,14 @@ module Gitlab
paid_signup_flow: {
feature_toggle: :paid_signup_flow,
environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.1,
+ enabled_ratio: 0.5,
tracking_category: 'Growth::Acquisition::Experiment::PaidSignUpFlow'
+ },
+ suggest_pipeline: {
+ feature_toggle: :suggest_pipeline,
+ environment: ::Gitlab.dev_env_or_com?,
+ enabled_ratio: 0.1,
+ tracking_category: 'Growth::Expansion::Experiment::SuggestPipeline'
}
}.freeze
@@ -53,14 +59,14 @@ module Gitlab
Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
end
- def track_experiment_event(experiment_key, action)
- track_experiment_event_for(experiment_key, action) do |tracking_data|
+ def track_experiment_event(experiment_key, action, value = nil)
+ track_experiment_event_for(experiment_key, action, value) do |tracking_data|
::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
end
end
- def frontend_experimentation_tracking_data(experiment_key, action)
- track_experiment_event_for(experiment_key, action) do |tracking_data|
+ def frontend_experimentation_tracking_data(experiment_key, action, value = nil)
+ track_experiment_event_for(experiment_key, action, value) do |tracking_data|
gon.push(tracking_data: tracking_data)
end
end
@@ -77,19 +83,20 @@ module Gitlab
experimentation_subject_id.delete('-').hex % 100
end
- def track_experiment_event_for(experiment_key, action)
+ def track_experiment_event_for(experiment_key, action, value)
return unless Experimentation.enabled?(experiment_key)
- yield experimentation_tracking_data(experiment_key, action)
+ yield experimentation_tracking_data(experiment_key, action, value)
end
- def experimentation_tracking_data(experiment_key, action)
+ def experimentation_tracking_data(experiment_key, action, value)
{
category: tracking_category(experiment_key),
action: action,
property: tracking_group(experiment_key),
- label: experimentation_subject_id
- }
+ label: experimentation_subject_id,
+ value: value
+ }.compact
end
def tracking_category(experiment_key)
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index a71baadfdb3..d438b0415fa 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -13,14 +13,15 @@ module Gitlab
@ref = ref
end
- def find(query)
+ def find(query, content_match_cutoff: nil)
query = Gitlab::Search::Query.new(query, encode_binary: true) do
filter :filename, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}$/i }
filter :path, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}/i }
filter :extension, matcher: ->(filter, blob) { blob.binary_path =~ /\.#{filter[:regex_value]}$/i }
end
- files = find_by_path(query.term) + find_by_content(query.term)
+ content_match_cutoff = nil if query.filters.any?
+ files = find_by_path(query.term) + find_by_content(query.term, { limit: content_match_cutoff })
files = query.filter_results(files) if query.filters.any?
@@ -29,8 +30,8 @@ module Gitlab
private
- def find_by_content(query)
- repository.search_files_by_content(query, ref).map do |result|
+ def find_by_content(query, options)
+ repository.search_files_by_content(query, ref, options).map do |result|
Gitlab::Search::FoundBlob.new(content_match: result, project: project, ref: ref, repository: repository)
end
end
diff --git a/lib/gitlab/file_hook.rb b/lib/gitlab/file_hook.rb
index f886fd10f53..38c19ff506f 100644
--- a/lib/gitlab/file_hook.rb
+++ b/lib/gitlab/file_hook.rb
@@ -17,7 +17,7 @@ module Gitlab
def self.execute_all_async(data)
args = files.map { |file| [file, data] }
- FileHookWorker.bulk_perform_async(args)
+ FileHookWorker.bulk_perform_async(args) # rubocop:disable Scalability/BulkPerformWithContext
end
def self.execute(file, data)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 10df4ed72d9..f2a6211f270 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -5,6 +5,7 @@ module Gitlab
class Blob
include Gitlab::BlobHelper
include Gitlab::EncodingHelper
+ include Gitlab::Metrics::Methods
extend Gitlab::Git::WrapsGitalyErrors
# This number is the maximum amount of data that we want to display to
@@ -13,6 +14,11 @@ module Gitlab
# use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10.megabytes
+ # The number of blobs loaded in a single Gitaly call
+ # When a large number of blobs requested, we'd want to fetch them in
+ # multiple Gitaly calls
+ BATCH_SIZE = 250
+
# These limits are used as a heuristic to ignore files which can't be LFS
# pointers. The format of these is described in
# https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
@@ -21,6 +27,14 @@ module Gitlab
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
+ define_counter :gitlab_blob_truncated_true do
+ docstring 'blob.truncated? == true'
+ end
+
+ define_counter :gitlab_blob_truncated_false do
+ docstring 'blob.truncated? == false'
+ end
+
class << self
def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
tree_entry(repository, sha, path, limit)
@@ -67,7 +81,13 @@ module Gitlab
# to the caller to limit the number of blobs and blob_size_limit.
#
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
- repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
+ if Feature.enabled?(:blobs_fetch_in_batches, default_enabled: true)
+ blob_references.each_slice(BATCH_SIZE).flat_map do |refs|
+ repository.gitaly_blob_client.get_blobs(refs, blob_size_limit).to_a
+ end
+ else
+ repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
+ end
end
# Returns an array of Blob instances just with the metadata, that means
@@ -117,7 +137,7 @@ module Gitlab
def load_all_data!(repository)
return if @data == '' # don't mess with submodule blobs
- # Even if we return early, recalculate wether this blob is binary in
+ # Even if we return early, recalculate whether this blob is binary in
# case a blob was initialized as text but the full data isn't
@binary = nil
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 48da838366f..0b999197cd8 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -130,8 +130,7 @@ module Gitlab
# :skip is the number of commits to skip
# :order is the commits order and allowed value is :none (default), :date,
# :topo, or any combination of them (in an array). Commit ordering types
- # are documented here:
- # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
+ # are documented here: https://git-scm.com/docs/git-log#_commit_ordering
def find_all(repo, options = {})
wrapped_gitaly_errors do
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index b79e30bff78..46896961867 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -102,7 +102,7 @@ module Gitlab
def populate!
return if @populated
- each { nil } # force a loop through all diffs
+ each {} # force a loop through all diffs
nil
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 0120e3be14c..6bfe744a5cd 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -636,10 +636,9 @@ module Gitlab
end
# Delete the specified branch from the repository
+ # Note: No Git hooks are executed for this action
def delete_branch(branch_name)
- wrapped_gitaly_errors do
- gitaly_ref_client.delete_branch(branch_name)
- end
+ write_ref(branch_name, Gitlab::Git::BLANK_SHA)
rescue CommandError => e
raise DeleteBranchError, e
end
@@ -651,14 +650,13 @@ module Gitlab
end
# Create a new branch named **ref+ based on **stat_point+, HEAD by default
+ # Note: No Git hooks are executed for this action
#
# Examples:
# create_branch("feature")
# create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD")
- wrapped_gitaly_errors do
- gitaly_ref_client.create_branch(ref, start_point)
- end
+ write_ref(ref, start_point)
end
# If `mirror_refmap` is present the remote is set as mirror with that mapping
@@ -822,17 +820,6 @@ module Gitlab
gitaly_repository_client.create_from_snapshot(url, auth)
end
- # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
- def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
- wrapped_gitaly_errors do
- gitaly_operation_client.user_rebase(user, rebase_id,
- branch: branch,
- branch_sha: branch_sha,
- remote_repository: remote_repository,
- remote_branch: remote_branch)
- end
- end
-
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: [], &block)
wrapped_gitaly_errors do
gitaly_operation_client.rebase(
@@ -969,13 +956,13 @@ module Gitlab
gitaly_ref_client.tag_names_contains_sha(sha)
end
- def search_files_by_content(query, ref)
+ def search_files_by_content(query, ref, options = {})
return [] if empty? || query.blank?
safe_query = Regexp.escape(query)
ref ||= root_ref
- gitaly_repository_client.search_files_by_content(ref, safe_query)
+ gitaly_repository_client.search_files_by_content(ref, safe_query, options)
end
def can_be_merged?(source_sha, target_branch)
diff --git a/lib/gitlab/git/rugged_impl/use_rugged.rb b/lib/gitlab/git/rugged_impl/use_rugged.rb
index 068aaf03c51..f63e35030c1 100644
--- a/lib/gitlab/git/rugged_impl/use_rugged.rb
+++ b/lib/gitlab/git/rugged_impl/use_rugged.rb
@@ -16,7 +16,9 @@ module Gitlab
end
def running_puma_with_multiple_threads?
- Gitlab::Runtime.puma? && ::Puma.cli_config.options[:max_threads] > 1
+ return false unless Gitlab::Runtime.puma?
+
+ ::Puma.respond_to?(:cli_config) && ::Puma.cli_config.options[:max_threads] > 1
end
def execute_rugged_call(method_name, *args)
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7e9ec097ef7..906350e57c5 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -50,8 +50,8 @@ module Gitlab
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
- @namespace_path = namespace_path
- @project_path = project_path
+ @namespace_path = namespace_path || project&.namespace&.full_path
+ @project_path = project_path || project&.path
@redirected_path = redirected_path
@auth_result_type = auth_result_type
end
@@ -60,6 +60,7 @@ module Gitlab
@logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER)
@changes = changes
+ check_namespace!
check_protocol!
check_valid_actor!
check_active_user!
@@ -136,6 +137,12 @@ module Gitlab
end
end
+ def check_namespace!
+ return if namespace_path.present?
+
+ raise NotFoundError, ERROR_MESSAGES[:project_not_found]
+ end
+
def check_active_user!
return unless user
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
new file mode 100644
index 00000000000..d99b9c3fe89
--- /dev/null
+++ b/lib/gitlab/git_access_snippet.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class GitAccessSnippet < GitAccess
+ ERROR_MESSAGES = {
+ snippet_not_found: 'The snippet you were looking for could not be found.',
+ repository_not_found: 'The snippet repository you were looking for could not be found.'
+ }.freeze
+
+ attr_reader :snippet
+
+ def initialize(actor, snippet, protocol, **kwargs)
+ @snippet = snippet
+
+ super(actor, project, protocol, **kwargs)
+ end
+
+ def check(cmd, _changes)
+ unless Feature.enabled?(:version_snippets, user)
+ raise NotFoundError, ERROR_MESSAGES[:snippet_not_found]
+ end
+
+ check_snippet_accessibility!
+
+ success_result(cmd)
+ end
+
+ def project
+ snippet&.project
+ end
+
+ private
+
+ def repository
+ snippet&.repository
+ end
+
+ def check_snippet_accessibility!
+ if snippet.blank?
+ raise NotFoundError, ERROR_MESSAGES[:snippet_not_found]
+ end
+
+ unless repository&.exists?
+ raise NotFoundError, ERROR_MESSAGES[:repository_not_found]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 262a1ef653f..4eb1ccf32ba 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -432,10 +432,7 @@ module Gitlab
end
def self.filesystem_id(storage)
- response = Gitlab::GitalyClient::ServerService.new(storage).info
- storage_status = response.storage_statuses.find { |status| status.storage_name == storage }
-
- storage_status&.filesystem_id
+ Gitlab::GitalyClient::ServerService.new(storage).storage_info&.filesystem_id
end
def self.filesystem_id_from_disk(storage)
@@ -446,6 +443,14 @@ module Gitlab
nil
end
+ def self.filesystem_disk_available(storage)
+ Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.available
+ end
+
+ def self.filesystem_disk_used(storage)
+ Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.used
+ end
+
def self.timeout(timeout_name)
Gitlab::CurrentSettings.current_application_settings[timeout_name]
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 15318bc817a..ac22f5bf419 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -324,6 +324,7 @@ module Gitlab
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = encode_binary(options[:ref]) if options[:ref]
+ request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present?
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 27522f89a5b..67fb0ab9608 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -203,36 +203,6 @@ module Gitlab
start_repository: start_repository)
end
- # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
- def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
- request = Gitaly::UserRebaseRequest.new(
- repository: @gitaly_repo,
- user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
- rebase_id: rebase_id.to_s,
- branch: encode_binary(branch),
- branch_sha: branch_sha,
- remote_repository: remote_repository.gitaly_repository,
- remote_branch: encode_binary(remote_branch)
- )
-
- response = GitalyClient.call(
- @repository.storage,
- :operation_service,
- :user_rebase,
- request,
- timeout: GitalyClient.long_timeout,
- remote_storage: remote_repository.storage
- )
-
- if response.pre_receive_error.presence
- raise Gitlab::Git::PreReceiveError, response.pre_receive_error
- elsif response.git_error.presence
- raise Gitlab::Git::Repository::GitError, response.git_error
- else
- response.rebase_sha
- end
- end
-
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: [])
request_enum = QueueEnumerator.new
rebase_sha = nil
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index d1f848fae26..63def4e29c9 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -151,40 +151,6 @@ module Gitlab
Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
end
- def create_branch(ref, start_point)
- request = Gitaly::CreateBranchRequest.new(
- repository: @gitaly_repo,
- name: encode_binary(ref),
- start_point: encode_binary(start_point)
- )
-
- response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request, timeout: GitalyClient.medium_timeout)
-
- case response.status
- when :OK
- branch = response.branch
- target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
- Gitlab::Git::Branch.new(@repository, branch.name, branch.target_commit.id, target_commit)
- when :ERR_INVALID
- invalid_ref!("Invalid ref name")
- when :ERR_EXISTS
- invalid_ref!("Branch #{ref} already exists")
- when :ERR_INVALID_START_POINT
- invalid_ref!("Invalid reference #{start_point}")
- else
- raise "Unknown response status: #{response.status}"
- end
- end
-
- def delete_branch(branch_name)
- request = Gitaly::DeleteBranchRequest.new(
- repository: @gitaly_repo,
- name: encode_binary(branch_name)
- )
-
- GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request, timeout: GitalyClient.medium_timeout)
- end
-
def delete_refs(refs: [], except_with_prefixes: [])
request = Gitaly::DeleteRefsRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index d0e5e0db830..597ae4651ea 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -332,11 +332,11 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
- def search_files_by_content(ref, query)
+ def search_files_by_content(ref, query, options = {})
request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
response = GitalyClient.call(@storage, :repository_service, :search_files_by_content, request, timeout: GitalyClient.default_timeout)
- search_results_from_response(response)
+ search_results_from_response(response, options)
end
def disconnect_alternates
@@ -361,18 +361,24 @@ module Gitlab
private
- def search_results_from_response(gitaly_response)
+ def search_results_from_response(gitaly_response, options = {})
+ limit = options[:limit]
+
matches = []
+ matches_count = 0
current_match = +""
gitaly_response.each do |message|
next if message.nil?
+ break if limit && matches_count >= limit
+
current_match << message.match_data
if message.end_of_match
matches << current_match
current_match = +""
+ matches_count += 1
end
end
diff --git a/lib/gitlab/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb
index 0ade6942db9..36bda67c26e 100644
--- a/lib/gitlab/gitaly_client/server_service.rb
+++ b/lib/gitlab/gitaly_client/server_service.rb
@@ -13,6 +13,24 @@ module Gitlab
def info
GitalyClient.call(@storage, :server_service, :server_info, Gitaly::ServerInfoRequest.new, timeout: GitalyClient.fast_timeout)
end
+
+ def disk_statistics
+ GitalyClient.call(@storage, :server_service, :disk_statistics, Gitaly::DiskStatisticsRequest.new, timeout: GitalyClient.fast_timeout)
+ end
+
+ def storage_info
+ storage_specific(info)
+ end
+
+ def storage_disk_statistics
+ storage_specific(disk_statistics)
+ end
+
+ private
+
+ def storage_specific(response)
+ response.storage_statuses.find { |status| status.storage_name == @storage }
+ end
end
end
end
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 99bf4258c07..fcebcb463cd 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -7,17 +7,25 @@ module Gitlab
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccess,
- repository_accessor: -> (project) { project.repository }
+ repository_resolver: -> (project) { project.repository }
).freeze
WIKI = RepoType.new(
name: :wiki,
access_checker_class: Gitlab::GitAccessWiki,
- repository_accessor: -> (project) { project.wiki.repository }
+ repository_resolver: -> (project) { project.wiki.repository },
+ suffix: :wiki
+ ).freeze
+ SNIPPET = RepoType.new(
+ name: :snippet,
+ access_checker_class: Gitlab::GitAccessSnippet,
+ repository_resolver: -> (snippet) { snippet.repository },
+ container_resolver: -> (id) { Snippet.find_by_id(id) }
).freeze
TYPES = {
PROJECT.name.to_s => PROJECT,
- WIKI.name.to_s => WIKI
+ WIKI.name.to_s => WIKI,
+ SNIPPET.name.to_s => SNIPPET
}.freeze
def self.types
@@ -27,15 +35,14 @@ module Gitlab
def self.parse(gl_repository)
type_name, _id = gl_repository.split('-').first
type = types[type_name]
- subject_id = type&.fetch_id(gl_repository)
- unless subject_id
+ unless type
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
end
- project = Project.find_by_id(subject_id)
+ container = type.fetch_container!(gl_repository)
- [project, type]
+ [container, type]
end
def self.default_type
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 01bc27f963b..9663fd7de8f 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -5,16 +5,25 @@ module Gitlab
class RepoType
attr_reader :name,
:access_checker_class,
- :repository_accessor
+ :repository_resolver,
+ :container_resolver,
+ :suffix
- def initialize(name:, access_checker_class:, repository_accessor:)
+ def initialize(
+ name:,
+ access_checker_class:,
+ repository_resolver:,
+ container_resolver: default_container_resolver,
+ suffix: nil)
@name = name
@access_checker_class = access_checker_class
- @repository_accessor = repository_accessor
+ @repository_resolver = repository_resolver
+ @container_resolver = container_resolver
+ @suffix = suffix
end
- def identifier_for_subject(subject)
- "#{name}-#{subject.id}"
+ def identifier_for_container(container)
+ "#{name}-#{container.id}"
end
def fetch_id(identifier)
@@ -22,6 +31,14 @@ module Gitlab
match[:id] if match
end
+ def fetch_container!(identifier)
+ id = fetch_id(identifier)
+
+ raise ArgumentError, "Invalid GL Repository \"#{identifier}\"" unless id
+
+ container_resolver.call(id)
+ end
+
def wiki?
self == WIKI
end
@@ -30,12 +47,26 @@ module Gitlab
self == PROJECT
end
+ def snippet?
+ self == SNIPPET
+ end
+
def path_suffix
- project? ? "" : ".#{name}"
+ suffix ? ".#{suffix}" : ''
end
- def repository_for(subject)
- repository_accessor.call(subject)
+ def repository_for(container)
+ repository_resolver.call(container)
+ end
+
+ def valid?(repository_path)
+ repository_path.end_with?(path_suffix)
+ end
+
+ private
+
+ def default_container_resolver
+ -> (id) { Project.find_by_id(id) }
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2e27e954e79..3db6c3b51c0 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -43,6 +43,9 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:snippets_vue, default_enabled: false)
+ push_frontend_feature_flag(:monaco_snippets, default_enabled: false)
+ push_frontend_feature_flag(:monaco_blobs, default_enabled: false)
+ push_frontend_feature_flag(:monaco_ci, default_enabled: false)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 7e6f6a519a6..8166bef4510 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -6,7 +6,7 @@ module Gitlab
CleanupError = Class.new(StandardError)
BG_CLEANUP_RUNTIME_S = 10
- FG_CLEANUP_RUNTIME_S = 0.5
+ FG_CLEANUP_RUNTIME_S = 1
MUTEX = Mutex.new
@@ -127,7 +127,10 @@ module Gitlab
# error.
# Failing to remove the tmp directory could leave the `gpg-agent` process
# running forever.
- Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1) do
+ #
+ # 15 tries will never complete within the maximum time with exponential
+ # backoff. So our limit is the runtime, not the number of tries.
+ Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1, tries: 15) do
FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir)
end
rescue => e
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index dc71d0b427a..1abbd6dc45b 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -2,36 +2,9 @@
module Gitlab
module Gpg
- class Commit
- include Gitlab::Utils::StrongMemoize
-
- def initialize(commit)
- @commit = commit
-
- repo = commit.project.repository.raw_repository
- @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
-
- lazy_signature
- end
-
- def signature_text
- strong_memoize(:signature_text) do
- @signature_data&.itself && @signature_data[0]
- end
- end
-
- def signed_text
- strong_memoize(:signed_text) do
- @signature_data&.itself && @signature_data[1]
- end
- end
-
- def has_signature?
- !!(signature_text && signed_text)
- end
-
+ class Commit < Gitlab::SignedCommit
def signature
- return unless has_signature?
+ super
return @signature if @signature
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
index 9bb1e8fc7a2..837473d47cd 100644
--- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -25,9 +25,12 @@ module Gitlab
def process_params(data)
return [] unless data.has_key?(:params)
- data[:params]
- .each_pair
- .map { |k, v| { key: k, value: utf8_encode_values(v) } }
+ params_array =
+ data[:params]
+ .each_pair
+ .map { |k, v| { key: k, value: utf8_encode_values(v) } }
+
+ Gitlab::Utils::LogLimitedArray.log_limited_array(params_array)
end
def utf8_encode_values(data)
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
index 22728cc0b65..26c9d77a8df 100644
--- a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
+++ b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
@@ -6,8 +6,16 @@ module Gitlab
module Keyset
module Conditions
class BaseCondition
- def initialize(arel_table, names, values, operator, before_or_after)
- @arel_table, @names, @values, @operator, @before_or_after = arel_table, names, values, operator, before_or_after
+ # @param [Arel::Table] arel_table for the relation being ordered
+ # @param [Array<OrderInfo>] order_list of extracted orderings
+ # @param [Array] values from the decoded cursor
+ # @param [Array<String>] operators determining sort comparison
+ # @param [Symbol] before_or_after indicates whether we want
+ # items :before the cursor or :after the cursor
+ def initialize(arel_table, order_list, values, operators, before_or_after)
+ @arel_table, @order_list, @values, @operators, @before_or_after = arel_table, order_list, values, operators, before_or_after
+
+ @before_or_after = :after unless [:after, :before].include?(@before_or_after)
end
def build
@@ -16,20 +24,27 @@ module Gitlab
private
- attr_reader :arel_table, :names, :values, :operator, :before_or_after
+ attr_reader :arel_table, :order_list, :values, :operators, :before_or_after
+
+ def table_condition(order_info, value, operator)
+ if order_info.named_function
+ target = order_info.named_function
+ value = value&.downcase if target&.name&.downcase == 'lower'
+ else
+ target = arel_table[order_info.attribute_name]
+ end
- def table_condition(attribute, value, operator)
case operator
when '>'
- arel_table[attribute].gt(value)
+ target.gt(value)
when '<'
- arel_table[attribute].lt(value)
+ target.lt(value)
when '='
- arel_table[attribute].eq(value)
+ target.eq(value)
when 'is_null'
- arel_table[attribute].eq(nil)
+ target.eq(nil)
when 'is_not_null'
- arel_table[attribute].not_eq(nil)
+ target.not_eq(nil)
end
end
end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
index 3b56ddb996d..3239d27c0cd 100644
--- a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
+++ b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
@@ -12,7 +12,7 @@ module Gitlab
# If there is only one order field, we can assume it
# does not contain NULLs, and don't need additional
# conditions
- unless names.count == 1
+ unless order_list.count == 1
conditions << [second_attribute_condition, final_condition]
end
@@ -24,7 +24,7 @@ module Gitlab
# ex: "(relative_position > 23)"
def first_attribute_condition
<<~SQL
- (#{table_condition(names.first, values.first, operator.first).to_sql})
+ (#{table_condition(order_list.first, values.first, operators.first).to_sql})
SQL
end
@@ -32,9 +32,9 @@ module Gitlab
def second_attribute_condition
condition = <<~SQL
OR (
- #{table_condition(names.first, values.first, '=').to_sql}
+ #{table_condition(order_list.first, values.first, '=').to_sql}
AND
- #{table_condition(names[1], values[1], operator[1]).to_sql}
+ #{table_condition(order_list[1], values[1], operators[1]).to_sql}
)
SQL
@@ -45,7 +45,7 @@ module Gitlab
def final_condition
if before_or_after == :after
<<~SQL
- OR (#{table_condition(names.first, nil, 'is_null').to_sql})
+ OR (#{table_condition(order_list.first, nil, 'is_null').to_sql})
SQL
end
end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
index 71a74936d5d..18ea0692e2c 100644
--- a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
+++ b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
@@ -16,9 +16,9 @@ module Gitlab
def first_attribute_condition
condition = <<~SQL
(
- #{table_condition(names.first, nil, 'is_null').to_sql}
+ #{table_condition(order_list.first, nil, 'is_null').to_sql}
AND
- #{table_condition(names[1], values[1], operator[1]).to_sql}
+ #{table_condition(order_list[1], values[1], operators[1]).to_sql}
)
SQL
@@ -29,7 +29,7 @@ module Gitlab
def final_condition
if before_or_after == :before
<<~SQL
- OR (#{table_condition(names.first, nil, 'is_not_null').to_sql})
+ OR (#{table_condition(order_list.first, nil, 'is_not_null').to_sql})
SQL
end
end
diff --git a/lib/gitlab/graphql/connections/keyset/order_info.rb b/lib/gitlab/graphql/connections/keyset/order_info.rb
index 4d85e8f79b7..7f61bf937b4 100644
--- a/lib/gitlab/graphql/connections/keyset/order_info.rb
+++ b/lib/gitlab/graphql/connections/keyset/order_info.rb
@@ -5,15 +5,15 @@ module Gitlab
module Connections
module Keyset
class OrderInfo
- attr_reader :attribute_name, :sort_direction
+ attr_reader :attribute_name, :sort_direction, :named_function
def initialize(order_value)
- if order_value.is_a?(String)
- @attribute_name, @sort_direction = extract_nulls_last_order(order_value)
- else
- @attribute_name = order_value.expr.name
- @sort_direction = order_value.direction
- end
+ @attribute_name, @sort_direction, @named_function =
+ if order_value.is_a?(String)
+ extract_nulls_last_order(order_value)
+ else
+ extract_attribute_values(order_value)
+ end
end
def operator_for(before_or_after)
@@ -69,7 +69,24 @@ module Gitlab
def extract_nulls_last_order(order_value)
tokens = order_value.downcase.split
- [tokens.first, (tokens[1] == 'asc' ? :asc : :desc)]
+ [tokens.first, (tokens[1] == 'asc' ? :asc : :desc), nil]
+ end
+
+ def extract_attribute_values(order_value)
+ named = nil
+ name = if ordering_by_lower?(order_value)
+ named = order_value.expr
+ named.expressions[0].name.to_s
+ else
+ order_value.expr.name
+ end
+
+ [name, order_value.direction, named]
+ end
+
+ # determine if ordering using LOWER, eg. "ORDER BY LOWER(boards.name)"
+ def ordering_by_lower?(order_value)
+ order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower'
end
end
end
diff --git a/lib/gitlab/graphql/connections/keyset/query_builder.rb b/lib/gitlab/graphql/connections/keyset/query_builder.rb
index e93c25d85fc..fe85898f638 100644
--- a/lib/gitlab/graphql/connections/keyset/query_builder.rb
+++ b/lib/gitlab/graphql/connections/keyset/query_builder.rb
@@ -40,17 +40,16 @@ module Gitlab
# "issues"."id" > 500
#
def conditions
- attr_names = order_list.map { |field| field.attribute_name }
- attr_values = attr_names.map { |name| decoded_cursor[name] }
+ attr_values = order_list.map { |field| decoded_cursor[field.attribute_name] }
- if attr_names.count == 1 && attr_values.first.nil?
+ if order_list.count == 1 && attr_values.first.nil?
raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value')
end
- if attr_names.count == 1 || attr_values.first.present?
- Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ if order_list.count == 1 || attr_values.first.present?
+ Keyset::Conditions::NotNullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build
else
- Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ Keyset::Conditions::NullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build
end
end
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index ac2a78c0f28..56524120ffd 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -21,6 +21,10 @@ module Gitlab
MD
end
+ def sorted_fields(fields)
+ fields.sort_by { |field| field[:name] }
+ end
+
# Some fields types are arrays of other types and are displayed
# on docs wrapped in square brackets, for example: [String!].
# This makes GitLab docs renderer thinks they are links so here
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index 52568286dca..b126a22c301 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -9,8 +9,8 @@
The API can be explored interactively using the [GraphiQL IDE](../index.md#graphiql).
-Each table below documents a GraphQL type. Types match loosely to models, but not all
-fields and methods on a model are available via GraphQL.
+ Each table below documents a GraphQL type. Types match loosely to models, but not all
+ fields and methods on a model are available via GraphQL.
\
- objects.each do |type|
- unless type[:fields].empty?
@@ -21,6 +21,6 @@ fields and methods on a model are available via GraphQL.
\
~ "| Name | Type | Description |"
~ "| --- | ---- | ---------- |"
- - type[:fields].each do |field|
+ - sorted_fields(type[:fields]).each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
\
diff --git a/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb b/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb
new file mode 100644
index 00000000000..1adedb500e6
--- /dev/null
+++ b/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+module Gitlab
+ module Graphql
+ module Extensions
+ class ExternallyPaginatedArrayExtension < GraphQL::Schema::Field::ConnectionExtension
+ def resolve(object:, arguments:, context:)
+ yield(object, arguments)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb
index ccf9e597307..79a7104a2ff 100644
--- a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb
+++ b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb
@@ -6,7 +6,7 @@ module Gitlab
module Graphql
module QueryAnalyzers
class RecursionAnalyzer
- IGNORED_FIELDS = %w(node edges ofType).freeze
+ IGNORED_FIELDS = %w(node edges nodes ofType).freeze
RECURSION_THRESHOLD = 2
def initial_value(query)
diff --git a/lib/gitlab/health_checks/base_abstract_check.rb b/lib/gitlab/health_checks/base_abstract_check.rb
index 199cd2f9b2d..7e1c5331b07 100644
--- a/lib/gitlab/health_checks/base_abstract_check.rb
+++ b/lib/gitlab/health_checks/base_abstract_check.rb
@@ -32,11 +32,9 @@ module Gitlab
end
def catch_timeout(seconds, &block)
- begin
- Timeout.timeout(seconds.to_i, &block)
- rescue Timeout::Error => ex
- ex
- end
+ Timeout.timeout(seconds.to_i, &block)
+ rescue Timeout::Error => ex
+ ex
end
end
end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 22b9a038768..07c43fa4832 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -32,7 +32,7 @@ module Gitlab
@lexer ||= custom_language || begin
Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new
rescue Rouge::Guesser::Ambiguous => e
- e.alternatives.sort_by(&:tag).first
+ e.alternatives.min_by(&:tag)
end
end
diff --git a/lib/gitlab/import_export/base_relation_factory.rb b/lib/gitlab/import_export/base_relation_factory.rb
index 562b549f6a1..d3c8802bcce 100644
--- a/lib/gitlab/import_export/base_relation_factory.rb
+++ b/lib/gitlab/import_export/base_relation_factory.rb
@@ -24,7 +24,8 @@ module Gitlab
last_edited_by_id
merge_user_id
resolved_by_id
- closed_by_id owner_id
+ closed_by_id
+ owner_id
].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml
index 049d81f96a4..d4e0ff12373 100644
--- a/lib/gitlab/import_export/group_import_export.yml
+++ b/lib/gitlab/import_export/group_import_export.yml
@@ -8,10 +8,14 @@ tree:
- :milestones
- :badges
- labels:
- - :priorities
- - :boards
+ - :priorities
+ - boards:
+ - lists:
+ - label:
+ - :priorities
+ - :board
- members:
- - :user
+ - :user
included_attributes:
user:
@@ -24,16 +28,28 @@ included_attributes:
excluded_attributes:
group:
- :id
+ - :owner_id
+ - :parent_id
+ - :created_at
+ - :updated_at
- :runners_token
- :runners_token_encrypted
+ - :saml_discovery_token
+ - :visibility_level
methods:
labels:
- :type
+ label:
+ - :type
badges:
- :type
notes:
- :type
+ events:
+ - :action
+ lists:
+ - :list_type
preloads:
@@ -43,10 +59,20 @@ ee:
tree:
group:
- epics:
- - :parent
- - notes:
- - :author
+ - :parent
+ - :award_emoji
+ - events:
+ - :push_event_payload
+ - notes:
+ - :author
+ - :award_emoji
+ - events:
+ - :push_event_payload
- boards:
- - :board_assignee
- - labels:
- - :priorities
+ - :board_assignee
+ - labels:
+ - :priorities
+ - lists:
+ - milestone:
+ - events:
+ - :push_event_payload
diff --git a/lib/gitlab/import_export/group_object_builder.rb b/lib/gitlab/import_export/group_object_builder.rb
new file mode 100644
index 00000000000..9796bfa07d4
--- /dev/null
+++ b/lib/gitlab/import_export/group_object_builder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ # Given a class, it finds or creates a new object at group level.
+ #
+ # Example:
+ # `GroupObjectBuilder.build(Label, label_attributes)`
+ # finds or initializes a label with the given attributes.
+ class GroupObjectBuilder < BaseObjectBuilder
+ def self.build(*args)
+ Group.transaction do
+ super
+ end
+ end
+
+ def initialize(klass, attributes)
+ super
+
+ @group = @attributes['group']
+
+ update_description
+ end
+
+ private
+
+ attr_reader :group
+
+ # Convert description empty string to nil
+ # due to existing object being saved with description: nil
+ # Which makes object lookup to fail since nil != ''
+ def update_description
+ attributes['description'] = nil if attributes['description'] == ''
+ end
+
+ def where_clauses
+ [
+ where_clause_base,
+ where_clause_for_title,
+ where_clause_for_description,
+ where_clause_for_created_at
+ ].compact
+ end
+
+ # Returns Arel clause `"{table_name}"."group_id" = {group.id}`
+ def where_clause_base
+ table[:group_id].in(group_and_ancestor_ids)
+ end
+
+ def group_and_ancestor_ids
+ group.ancestors.map(&:id) << group.id
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
index d6d780f165e..9e8f9d11393 100644
--- a/lib/gitlab/import_export/group_project_object_builder.rb
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -50,7 +50,7 @@ module Gitlab
def where_clause_base
[].tap do |clauses|
clauses << table[:project_id].eq(project.id) if project
- clauses << table[:group_id].eq(group.id) if group
+ clauses << table[:group_id].in(group.self_and_ancestors_ids) if group
end.reduce(:or)
end
@@ -60,7 +60,9 @@ module Gitlab
end
def prepare_attributes
- attributes.except('group').tap do |atts|
+ attributes.dup.tap do |atts|
+ atts.delete('group') unless epic?
+
if label?
atts['type'] = 'ProjectLabel' # Always create project labels
elsif milestone?
diff --git a/lib/gitlab/import_export/group_relation_factory.rb b/lib/gitlab/import_export/group_relation_factory.rb
new file mode 100644
index 00000000000..e3597af44d2
--- /dev/null
+++ b/lib/gitlab/import_export/group_relation_factory.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class GroupRelationFactory < BaseRelationFactory
+ OVERRIDES = {
+ labels: :group_labels,
+ priorities: :label_priorities,
+ label: :group_label,
+ parent: :epic
+ }.freeze
+
+ EXISTING_OBJECT_RELATIONS = %i[
+ epic
+ epics
+ milestone
+ milestones
+ label
+ labels
+ group_label
+ group_labels
+ ].freeze
+
+ private
+
+ def setup_models
+ setup_note if @relation_name == :notes
+
+ update_group_references
+ end
+
+ def update_group_references
+ return unless self.class.existing_object_relations.include?(@relation_name)
+ return unless @relation_hash['group_id']
+
+ @relation_hash['group_id'] = @importable.id
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group_tree_restorer.rb b/lib/gitlab/import_export/group_tree_restorer.rb
new file mode 100644
index 00000000000..2f42843ed6c
--- /dev/null
+++ b/lib/gitlab/import_export/group_tree_restorer.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class GroupTreeRestorer
+ attr_reader :user
+ attr_reader :shared
+ attr_reader :group
+
+ def initialize(user:, shared:, group:, group_hash:)
+ @path = File.join(shared.export_path, 'group.json')
+ @user = user
+ @shared = shared
+ @group = group
+ @group_hash = group_hash
+ end
+
+ def restore
+ @tree_hash = @group_hash || read_tree_hash
+ @group_members = @tree_hash.delete('members')
+ @children = @tree_hash.delete('children')
+
+ if members_mapper.map && restorer.restore
+ @children&.each do |group_hash|
+ group = create_group(group_hash: group_hash, parent_group: @group)
+ shared = Gitlab::ImportExport::Shared.new(group)
+
+ self.class.new(
+ user: @user,
+ shared: shared,
+ group: group,
+ group_hash: group_hash
+ ).restore
+ end
+ end
+
+ return false if @shared.errors.any?
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def read_tree_hash
+ json = IO.read(@path)
+ ActiveSupport::JSON.decode(json)
+ rescue => e
+ @shared.logger.error(
+ group_id: @group.id,
+ group_name: @group.name,
+ message: "Import/Export error: #{e.message}"
+ )
+
+ raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
+ end
+
+ def restorer
+ @relation_tree_restorer ||= RelationTreeRestorer.new(
+ user: @user,
+ shared: @shared,
+ importable: @group,
+ tree_hash: @tree_hash.except('name', 'path'),
+ members_mapper: members_mapper,
+ object_builder: object_builder,
+ relation_factory: relation_factory,
+ reader: reader
+ )
+ end
+
+ def create_group(group_hash:, parent_group:)
+ group_params = {
+ name: group_hash['name'],
+ path: group_hash['path'],
+ parent_id: parent_group&.id,
+ visibility_level: sub_group_visibility_level(group_hash, parent_group)
+ }
+
+ ::Groups::CreateService.new(@user, group_params).execute
+ end
+
+ def sub_group_visibility_level(group_hash, parent_group)
+ original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE
+
+ if parent_group && parent_group.visibility_level < original_visibility_level
+ Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level)
+ else
+ original_visibility_level
+ end
+ end
+
+ def members_mapper
+ @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
+ end
+
+ def relation_factory
+ Gitlab::ImportExport::GroupRelationFactory
+ end
+
+ def object_builder
+ Gitlab::ImportExport::GroupObjectBuilder
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2acb79e3e22..e55ad898263 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -25,8 +25,6 @@ tree:
- milestone:
- events:
- :push_event_payload
- - issue_milestones:
- - :milestone
- resource_label_events:
- label:
- :priorities
@@ -64,8 +62,6 @@ tree:
- milestone:
- events:
- :push_event_payload
- - merge_request_milestones:
- - :milestone
- resource_label_events:
- label:
- :priorities
@@ -178,7 +174,6 @@ excluded_attributes:
- :encrypted_secret_token
- :encrypted_secret_token_iv
- :repository_storage
- - :storage_version
merge_request_diff:
- :external_diff
- :stored_externally
@@ -213,12 +208,6 @@ excluded_attributes:
- :latest_merge_request_diff_id
- :head_pipeline_id
- :state_id
- issue_milestones:
- - :milestone_id
- - :issue_id
- merge_request_milestones:
- - :milestone_id
- - :merge_request_id
award_emoji:
- :awardable_id
statuses:
diff --git a/lib/gitlab/import_export/import_failure_service.rb b/lib/gitlab/import_export/import_failure_service.rb
index eeaf10870c8..d4eca551b49 100644
--- a/lib/gitlab/import_export/import_failure_service.rb
+++ b/lib/gitlab/import_export/import_failure_service.rb
@@ -12,9 +12,14 @@ module Gitlab
@association = importable.association(:import_failures)
end
- def with_retry(relation_key, relation_index)
+ def with_retry(action:, relation_key: nil, relation_index: nil)
on_retry = -> (exception, retry_count, *_args) do
- log_import_failure(relation_key, relation_index, exception, retry_count)
+ log_import_failure(
+ source: action,
+ relation_key: relation_key,
+ relation_index: relation_index,
+ exception: exception,
+ retry_count: retry_count)
end
Retriable.with_context(:relation_import, on_retry: on_retry) do
@@ -22,8 +27,9 @@ module Gitlab
end
end
- def log_import_failure(relation_key, relation_index, exception, retry_count = 0)
+ def log_import_failure(source:, relation_key: nil, relation_index: nil, exception:, retry_count: 0)
extra = {
+ source: source,
relation_key: relation_key,
relation_index: relation_index,
retry_count: retry_count
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index d2e27388b51..2a70344374b 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -9,7 +9,7 @@ module Gitlab
@importable = importable
# This needs to run first, as second call would be from #map
- # which means project members already exist.
+ # which means Project/Group members already exist.
ensure_default_member!
end
@@ -47,6 +47,8 @@ module Gitlab
end
def ensure_default_member!
+ return if user_already_member?
+
@importable.members.destroy_all # rubocop: disable DestroyAll
relation_class.create!(user: @user, access_level: relation_class::MAINTAINER, source_id: @importable.id, importing: true)
@@ -54,6 +56,12 @@ module Gitlab
raise e, "Error adding importer user to #{@importable.class} members. #{e.message}"
end
+ def user_already_member?
+ member = @importable.members&.first
+
+ member&.user == @user && member.access_level >= relation_class::MAINTAINER
+ end
+
def add_team_member(member, existing_user = nil)
member['user'] = existing_user
@@ -74,7 +82,7 @@ module Gitlab
end
def find_user_query(member)
- user_arel[:email].eq(member['user']['email']).or(user_arel[:username].eq(member['user']['username']))
+ user_arel[:email].eq(member['user']['email'])
end
def user_arel
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index 0b534a5bafc..f735b9612aa 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -16,7 +16,7 @@ module Gitlab
if fork_merge_request? && @diff_head_sha
@merge_request.source_project_id = @relation_hash['project_id']
- fetch_ref unless branch_exists?(@merge_request.source_branch)
+ create_source_branch unless branch_exists?(@merge_request.source_branch)
create_target_branch unless branch_exists?(@merge_request.target_branch)
end
@@ -34,17 +34,18 @@ module Gitlab
@merge_request
end
- def create_target_branch
- @project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha)
+ # When the exported MR was in a fork, the source branch does not exist in
+ # the imported bundle - although the commits usually do - so it must be
+ # created manually. Ignore failures so we get the merge request itself if
+ # the commits are missing.
+ def create_source_branch
+ @project.repository.create_branch(@merge_request.source_branch, @diff_head_sha)
+ rescue => err
+ Rails.logger.warn("Import/Export warning: Failed to create source branch #{@merge_request.source_branch} => #{@diff_head_sha} for MR #{@merge_request.iid}: #{err}") # rubocop:disable Gitlab/RailsLogger
end
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1295
- def fetch_ref
- target_ref = Gitlab::Git::BRANCH_REF_PREFIX + @merge_request.source_branch
-
- unless @project.repository.fetch_source_branch!(@project.repository, @diff_head_sha, target_ref)
- Rails.logger.warn("Import/Export warning: Failed to create #{target_ref} for MR: #{@merge_request.iid}") # rubocop:disable Gitlab/RailsLogger
- end
+ def create_target_branch
+ @project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha)
end
def branch_exists?(branch_name)
diff --git a/lib/gitlab/import_export/project_tree_loader.rb b/lib/gitlab/import_export/project_tree_loader.rb
new file mode 100644
index 00000000000..fc21858043d
--- /dev/null
+++ b/lib/gitlab/import_export/project_tree_loader.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class ProjectTreeLoader
+ def load(path, dedup_entries: false)
+ tree_hash = ActiveSupport::JSON.decode(IO.read(path))
+
+ if dedup_entries
+ dedup_tree(tree_hash)
+ else
+ tree_hash
+ end
+ end
+
+ private
+
+ # This function removes duplicate entries from the given tree recursively
+ # by caching nodes it encounters repeatedly. We only consider nodes for
+ # which there can actually be multiple equivalent instances (e.g. strings,
+ # hashes and arrays, but not `nil`s, numbers or booleans.)
+ #
+ # The algorithm uses a recursive depth-first descent with 3 cases, starting
+ # with a root node (the tree/hash itself):
+ # - a node has already been cached; in this case we return it from the cache
+ # - a node has not been cached yet but should be; descend into its children
+ # - a node is neither cached nor qualifies for caching; this is a no-op
+ def dedup_tree(node, nodes_seen = {})
+ if nodes_seen.key?(node) && distinguishable?(node)
+ yield nodes_seen[node]
+ elsif should_dedup?(node)
+ nodes_seen[node] = node
+
+ case node
+ when Array
+ node.each_index do |idx|
+ dedup_tree(node[idx], nodes_seen) do |cached_node|
+ node[idx] = cached_node
+ end
+ end
+ when Hash
+ node.each do |k, v|
+ dedup_tree(v, nodes_seen) do |cached_node|
+ node[k] = cached_node
+ end
+ end
+ end
+ else
+ node
+ end
+ end
+
+ # We do not need to consider nodes for which there cannot be multiple instances
+ def should_dedup?(node)
+ node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
+ end
+
+ # We can only safely de-dup values that are distinguishable. True value objects
+ # are always distinguishable by nature. Hashes however can represent entities,
+ # which are identified by ID, not value. We therefore disallow de-duping hashes
+ # that do not have an `id` field, since we might risk dropping entities that
+ # have equal attributes yet different identities.
+ def distinguishable?(node)
+ if node.is_a?(Hash)
+ node.key?('id')
+ else
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index e598cfc143e..aae07657ea0 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -3,15 +3,17 @@
module Gitlab
module ImportExport
class ProjectTreeRestorer
+ LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
+
attr_reader :user
attr_reader :shared
attr_reader :project
def initialize(user:, shared:, project:)
- @path = File.join(shared.export_path, 'project.json')
@user = user
@shared = shared
@project = project
+ @tree_loader = ProjectTreeLoader.new
end
def restore
@@ -21,7 +23,9 @@ module Gitlab
RelationRenameService.rename(@tree_hash)
if relation_tree_restorer.restore
- @project.merge_requests.set_latest_merge_request_diff_ids!
+ import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
+ @project.merge_requests.set_latest_merge_request_diff_ids!
+ end
true
else
@@ -34,9 +38,16 @@ module Gitlab
private
+ def large_project?(path)
+ File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
+ end
+
def read_tree_hash
- json = IO.read(@path)
- ActiveSupport::JSON.decode(json)
+ path = File.join(@shared.export_path, 'project.json')
+ dedup_entries = large_project?(path) &&
+ Feature.enabled?(:dedup_project_import_metadata, project.group)
+
+ @tree_loader.load(path, dedup_entries: dedup_entries)
rescue => e
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
@@ -72,6 +83,10 @@ module Gitlab
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
+
+ def import_failure_service
+ @import_failure_service ||= ImportFailureService.new(@project)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index 44cf90fb86a..cc01d70db16 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -73,13 +73,17 @@ module Gitlab
relation_object.assign_attributes(importable_class_sym => @importable)
- import_failure_service.with_retry(relation_key, relation_index) do
+ import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
end
save_id_mapping(relation_key, data_hash, relation_object)
rescue => e
- import_failure_service.log_import_failure(relation_key, relation_index, e)
+ import_failure_service.log_import_failure(
+ source: 'process_relation_item!',
+ relation_key: relation_key,
+ relation_index: relation_index,
+ exception: e)
end
def import_failure_service
@@ -155,7 +159,7 @@ module Gitlab
def build_relation(relation_key, relation_definition, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
- return data_hash if relation_key == 'author'
+ return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
@@ -165,6 +169,13 @@ module Gitlab
@relation_factory.create(relation_factory_params(relation_key, data_hash))
end
+ # Since we update the data hash in place as we restore relation items,
+ # and since we also de-duplicate items, we might encounter items that
+ # have already been restored in a previous iteration.
+ def already_restored?(relation_item)
+ !relation_item.is_a?(Hash)
+ end
+
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index eece4edf895..4547a9b0a01 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -61,7 +61,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
- Regexp.new(/\A#{regex}\z/).freeze
+ Regexp.new(/\A<?#{regex}>?\z/).freeze
end
end
end
diff --git a/lib/gitlab/jira/http_client.rb b/lib/gitlab/jira/http_client.rb
index 0c8b509740c..c09d8170d17 100644
--- a/lib/gitlab/jira/http_client.rb
+++ b/lib/gitlab/jira/http_client.rb
@@ -12,7 +12,7 @@ module Gitlab
def request(*args)
result = make_request(*args)
- raise JIRA::HTTPError.new(result) unless result.response.is_a?(Net::HTTPSuccess)
+ raise JIRA::HTTPError.new(result.response) unless result.response.is_a?(Net::HTTPSuccess)
result
end
diff --git a/lib/gitlab/kubernetes/generic_secret.rb b/lib/gitlab/kubernetes/generic_secret.rb
new file mode 100644
index 00000000000..45adf869da0
--- /dev/null
+++ b/lib/gitlab/kubernetes/generic_secret.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class GenericSecret
+ attr_reader :name, :data, :namespace_name
+
+ def initialize(name, data, namespace_name)
+ @name = name
+ @data = data
+ @namespace_name = namespace_name
+ end
+
+ def generate
+ ::Kubeclient::Resource.new(
+ type: generic_secret_type,
+ metadata: metadata,
+ data: data
+ )
+ end
+
+ private
+
+ def generic_secret_type
+ 'Opaque'
+ end
+
+ def metadata
+ {
+ name: name,
+ namespace: namespace_name
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 7cb7f46a623..7c5525b982c 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -16,6 +16,7 @@ module Gitlab
SUPPORTED_API_GROUPS = {
core: { group: 'api', version: 'v1' },
rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' },
+ apps: { group: 'apis/apps', version: 'v1' },
extensions: { group: 'apis/extensions', version: 'v1beta1' },
istio: { group: 'apis/networking.istio.io', version: 'v1alpha3' },
knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' }
@@ -74,10 +75,6 @@ module Gitlab
:update_role_binding,
to: :rbac_client
- # Deployments resource is currently on the apis/extensions api group
- delegate :get_deployments,
- to: :extensions_client
-
# non-entity methods that can only work with the core client
# as it uses the pods/log resource
delegate :get_pod_log,
@@ -93,16 +90,40 @@ module Gitlab
attr_reader :api_prefix, :kubeclient_options
+ DEFAULT_KUBECLIENT_OPTIONS = {
+ timeouts: {
+ open: 10,
+ read: 30
+ }
+ }.freeze
+
# We disable redirects through 'http_max_redirects: 0',
# so that KubeClient does not follow redirects and
# expose internal services.
def initialize(api_prefix, **kubeclient_options)
@api_prefix = api_prefix
- @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0)
+ @kubeclient_options = DEFAULT_KUBECLIENT_OPTIONS
+ .deep_merge(kubeclient_options)
+ .merge(http_max_redirects: 0)
validate_url!
end
+ # Deployments resource is currently on the apis/extensions api group
+ # until Kubernetes 1.15. Kubernetest 1.16+ has deployments resources in
+ # the apis/apps api group.
+ #
+ # As we still support Kubernetes 1.12+, we will need to support both.
+ def get_deployments(**args)
+ extensions_client.discover unless extensions_client.discovered
+
+ if extensions_client.respond_to?(:get_deployments)
+ extensions_client.get_deployments(**args)
+ else
+ apps_client.get_deployments(**args)
+ end
+ end
+
def create_or_update_cluster_role_binding(resource)
if cluster_role_binding_exists?(resource)
update_cluster_role_binding(resource)
diff --git a/lib/gitlab/kubernetes/tls_secret.rb b/lib/gitlab/kubernetes/tls_secret.rb
new file mode 100644
index 00000000000..2895f4df27c
--- /dev/null
+++ b/lib/gitlab/kubernetes/tls_secret.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class TlsSecret
+ attr_reader :name, :cert, :key, :namespace_name
+
+ def initialize(name, cert, key, namespace_name)
+ @name = name
+ @cert = cert
+ @key = key
+ @namespace_name = namespace_name
+ end
+
+ def generate
+ ::Kubeclient::Resource.new(
+ type: tls_secret_type,
+ metadata: metadata,
+ data: data
+ )
+ end
+
+ private
+
+ def tls_secret_type
+ 'kubernetes.io/tls'
+ end
+
+ def metadata
+ {
+ name: name,
+ namespace: namespace_name
+ }
+ end
+
+ def data
+ {
+ 'tls.crt': Base64.strict_encode64(cert),
+ 'tls.key': Base64.strict_encode64(key)
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/log_timestamp_formatter.rb b/lib/gitlab/log_timestamp_formatter.rb
new file mode 100644
index 00000000000..433dedeb7a0
--- /dev/null
+++ b/lib/gitlab/log_timestamp_formatter.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class LogTimestampFormatter < Logger::Formatter
+ FORMAT = "%s, [%s #%d] %5s -- %s: %s\n"
+
+ def call(severity, timestamp, program_name, message)
+ FORMAT % [severity[0..0], timestamp.utc.iso8601(3), $$, severity, program_name, msg2str(message)]
+ end
+ end
+end
diff --git a/lib/gitlab/looping_batcher.rb b/lib/gitlab/looping_batcher.rb
new file mode 100644
index 00000000000..adf0aeda506
--- /dev/null
+++ b/lib/gitlab/looping_batcher.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ # Returns an ID range within a table so it can be iterated over. Repeats from
+ # the beginning after it reaches the end.
+ #
+ # Used by Geo in particular to iterate over a replicable and its registry
+ # table.
+ #
+ # Tracks a cursor for each table, by "key". If the table is smaller than
+ # batch_size, then a range for the whole table is returned on every call.
+ class LoopingBatcher
+ # @param [Class] model_class the class of the table to iterate on
+ # @param [String] key to identify the cursor. Note, cursor is already unique
+ # per table.
+ # @param [Integer] batch_size to limit the number of records in a batch
+ def initialize(model_class, key:, batch_size: 1000)
+ @model_class = model_class
+ @key = key
+ @batch_size = batch_size
+ end
+
+ # @return [Range] a range of IDs. `nil` if 0 records at or after the cursor.
+ def next_range!
+ return unless @model_class.any?
+
+ batch_first_id = cursor_id
+
+ batch_last_id = get_batch_last_id(batch_first_id)
+ return unless batch_last_id
+
+ batch_first_id..batch_last_id
+ end
+
+ private
+
+ # @private
+ #
+ # Get the last ID of the batch. Increment the cursor or reset it if at end.
+ #
+ # @param [Integer] batch_first_id the first ID of the batch
+ # @return [Integer] batch_last_id the last ID of the batch (not the table)
+ def get_batch_last_id(batch_first_id)
+ batch_last_id, more_rows = run_query(@model_class.table_name, @model_class.primary_key, batch_first_id, @batch_size)
+
+ if more_rows
+ increment_batch(batch_last_id)
+ else
+ reset if batch_first_id > 1
+ end
+
+ batch_last_id
+ end
+
+ def run_query(table, primary_key, batch_first_id, batch_size)
+ sql = <<~SQL
+ SELECT MAX(batch.id) AS batch_last_id,
+ EXISTS (
+ SELECT #{primary_key}
+ FROM #{table}
+ WHERE #{primary_key} > MAX(batch.id)
+ ) AS more_rows
+ FROM (
+ SELECT #{primary_key}
+ FROM #{table}
+ WHERE #{primary_key} >= #{batch_first_id}
+ ORDER BY #{primary_key}
+ LIMIT #{batch_size}) AS batch;
+ SQL
+
+ result = ActiveRecord::Base.connection.exec_query(sql).first
+
+ [result["batch_last_id"], result["more_rows"]]
+ end
+
+ def reset
+ set_cursor_id(1)
+ end
+
+ def increment_batch(batch_last_id)
+ set_cursor_id(batch_last_id + 1)
+ end
+
+ # @private
+ #
+ # @return [Integer] the cursor ID, or 1 if it is not set
+ def cursor_id
+ Rails.cache.fetch("#{cache_key}:cursor_id") || 1
+ end
+
+ def set_cursor_id(id)
+ Rails.cache.write("#{cache_key}:cursor_id", id)
+ end
+
+ def cache_key
+ @cache_key ||= "#{self.class.name.parameterize}:#{@model_class.name.parameterize}:#{@key}:cursor_id"
+ end
+ end
+end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index f7699ef1718..bd69843adf1 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -2,6 +2,7 @@
require 'yaml'
require 'json'
+require 'pathname'
require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues)
# This service is run independently of the main Rails process,
@@ -21,39 +22,60 @@ module Gitlab
log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log')
}.freeze
+ # Email specific configuration which is merged with configuration
+ # fetched from YML config file.
+ ADDRESS_SPECIFIC_CONFIG = {
+ incoming_email: {
+ queue: 'email_receiver',
+ worker: 'EmailReceiverWorker'
+ },
+ service_desk_email: {
+ queue: 'service_desk_email_receiver',
+ worker: 'ServiceDeskEmailReceiverWorker'
+ }
+ }.freeze
+
class << self
- def enabled?
- config[:enabled] && config[:address]
+ def enabled_configs
+ @enabled_configs ||= configs.select { |config| enabled?(config) }
end
- def config
- @config ||= fetch_config
- end
+ private
- def reset_config!
- @config = nil
+ def enabled?(config)
+ config[:enabled] && !config[:address].to_s.empty?
end
- private
+ def configs
+ ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) }
+ end
- def fetch_config
+ def fetch_config(config_key)
return {} unless File.exist?(config_file)
- config = load_from_yaml || {}
- config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval|
+ config = merged_configs(config_key)
+ config.merge!(redis_config) if enabled?(config)
+ config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR)
+
+ config
+ end
+
+ def merged_configs(config_key)
+ yml_config = load_yaml.fetch(config_key, {})
+ specific_config = ADDRESS_SPECIFIC_CONFIG.fetch(config_key, {})
+ DEFAULT_CONFIG.merge(specific_config, yml_config) do |_key, oldval, newval|
newval.nil? ? oldval : newval
end
+ end
- if config[:enabled] && config[:address]
- gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
- config[:redis_url] = gitlab_redis_queues.url
+ def redis_config
+ gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
+ config = { redis_url: gitlab_redis_queues.url }
- if gitlab_redis_queues.sentinels?
- config[:sentinels] = gitlab_redis_queues.sentinels
- end
+ if gitlab_redis_queues.sentinels?
+ config[:sentinels] = gitlab_redis_queues.sentinels
end
- config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR)
config
end
@@ -65,8 +87,8 @@ module Gitlab
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__)
end
- def load_from_yaml
- YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email]
+ def load_yaml
+ @yaml ||= YAML.load_file(config_file)[rails_env].deep_symbolize_keys
end
end
end
diff --git a/lib/gitlab/marginalia.rb b/lib/gitlab/marginalia.rb
index 2be96cecae3..24e21a1d512 100644
--- a/lib/gitlab/marginalia.rb
+++ b/lib/gitlab/marginalia.rb
@@ -2,6 +2,8 @@
module Gitlab
module Marginalia
+ cattr_accessor :enabled, default: false
+
MARGINALIA_FEATURE_FLAG = :marginalia
def self.set_application_name
@@ -15,14 +17,14 @@ module Gitlab
end
def self.cached_feature_enabled?
- !!@enabled
+ enabled
end
def self.set_feature_cache
# During db:create and db:bootstrap skip feature query as DB is not available yet.
- return false unless ActiveRecord::Base.connected? && Gitlab::Database.cached_table_exists?('features')
+ return false unless Gitlab::Database.cached_table_exists?('features')
- @enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG)
+ self.enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG)
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
index 268112f33a9..3dd86c8685d 100644
--- a/lib/gitlab/metrics/dashboard/finder.rb
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -34,6 +34,8 @@ module Gitlab
# cluster, one of [:admin, :project, :group]
# @param options - grafana_url [String] URL pointing
# to a grafana dashboard panel
+ # @param options - prometheus_alert_id [Integer] ID of
+ # a PrometheusAlert. For dashboard embeds.
# @return [Hash]
def find(project, user, options = {})
service_for(options)
@@ -63,7 +65,7 @@ module Gitlab
def find_all_paths_from_source(project)
Gitlab::Metrics::Dashboard::Cache.delete_all!
- system_service.all_dashboard_paths(project)
+ default_dashboard_path(project)
.+ project_service.all_dashboard_paths(project)
end
@@ -77,6 +79,18 @@ module Gitlab
::Metrics::Dashboard::ProjectDashboardService
end
+ def self_monitoring_service
+ ::Metrics::Dashboard::SelfMonitoringDashboardService
+ end
+
+ def default_dashboard_path(project)
+ if project.self_monitoring?
+ self_monitoring_service.all_dashboard_paths(project)
+ else
+ system_service.all_dashboard_paths(project)
+ end
+ end
+
def service_for(options)
Gitlab::Metrics::Dashboard::ServiceSelector.call(options)
end
diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb
index 5b6f25420e0..24ea85a5a95 100644
--- a/lib/gitlab/metrics/dashboard/service_selector.rb
+++ b/lib/gitlab/metrics/dashboard/service_selector.rb
@@ -8,50 +8,40 @@ module Gitlab
module Metrics
module Dashboard
class ServiceSelector
- SERVICES = ::Metrics::Dashboard
-
class << self
include Gitlab::Utils::StrongMemoize
+ SERVICES = [
+ ::Metrics::Dashboard::CustomMetricEmbedService,
+ ::Metrics::Dashboard::GrafanaMetricEmbedService,
+ ::Metrics::Dashboard::DynamicEmbedService,
+ ::Metrics::Dashboard::DefaultEmbedService,
+ ::Metrics::Dashboard::SystemDashboardService,
+ ::Metrics::Dashboard::PodDashboardService,
+ ::Metrics::Dashboard::SelfMonitoringDashboardService,
+ ::Metrics::Dashboard::ProjectDashboardService
+ ].freeze
+
# Returns a class which inherits from the BaseService
- # class that can be used to obtain a dashboard.
+ # class that can be used to obtain a dashboard for
+ # the provided params.
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
def call(params)
- return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
- return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params)
- return SERVICES::DynamicEmbedService if dynamic_embed?(params)
- return SERVICES::DefaultEmbedService if params[:embedded]
- return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
- return SERVICES::PodDashboardService if pod_dashboard?(params[:dashboard_path])
- return SERVICES::ProjectDashboardService if params[:dashboard_path]
-
- default_service
- end
+ service = services.find do |service_class|
+ service_class.valid_params?(params)
+ end
- private
-
- def default_service
- SERVICES::SystemDashboardService
+ service || default_service
end
- def system_dashboard?(filepath)
- SERVICES::SystemDashboardService.matching_dashboard?(filepath)
- end
-
- def pod_dashboard?(filepath)
- SERVICES::PodDashboardService.matching_dashboard?(filepath)
- end
-
- def custom_metric_embed?(params)
- SERVICES::CustomMetricEmbedService.valid_params?(params)
- end
+ private
- def grafana_metric_embed?(params)
- SERVICES::GrafanaMetricEmbedService.valid_params?(params)
+ def services
+ SERVICES
end
- def dynamic_embed?(params)
- SERVICES::DynamicEmbedService.valid_params?(params)
+ def default_service
+ ::Metrics::Dashboard::SystemDashboardService
end
end
end
diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb
new file mode 100644
index 00000000000..a9d11f58255
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class ProjectMetricsDetailsInserter < BaseStage
+ def transform!
+ dashboard[:panel_groups].each do |panel_group|
+ next unless panel_group
+
+ has_custom_metrics = custom_group_titles.include?(panel_group[:group])
+ panel_group[:has_custom_metrics] = has_custom_metrics
+
+ panel_group[:panels].each do |panel|
+ next unless panel
+
+ panel[:metrics].each do |metric|
+ next unless metric
+
+ metric[:edit_path] = has_custom_metrics ? edit_path(metric) : nil
+ end
+ end
+ end
+ end
+
+ private
+
+ def custom_group_titles
+ @custom_group_titles ||= PrometheusMetricEnums.custom_group_details.values.map { |group_details| group_details[:group_title] }
+ end
+
+ def edit_path(metric)
+ Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path(project, metric[:metric_id])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 712f769bbeb..1d948883151 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -6,41 +6,41 @@ module Gitlab
module Dashboard
class Url
class << self
+ include Gitlab::Utils::StrongMemoize
+
+ QUERY_PATTERN = '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
+ ANCHOR_PATTERN = '(?<anchor>\#[a-z0-9_-]+)?'
+ OPTIONAL_DASH_PATTERN = '(?:/-)?'
+
# Matches urls for a metrics dashboard. This could be
# either the /metrics endpoint or the /metrics_dashboard
# endpoint.
#
# EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics
- def regex
- %r{
- (?<url>
- #{gitlab_pattern}
- #{project_pattern}
- (?:\/\-)?
- \/environments
- \/(?<environment>\d+)
- \/metrics
- #{query_pattern}
- #{anchor_pattern}
+ def metrics_regex
+ strong_memoize(:metrics_regex) do
+ regex_for_project_metrics(
+ %r{
+ /environments
+ /(?<environment>\d+)
+ /metrics
+ }x
)
- }x
+ end
end
# Matches dashboard urls for a Grafana embed.
#
# EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard
def grafana_regex
- %r{
- (?<url>
- #{gitlab_pattern}
- #{project_pattern}
- (?:\/\-)?
- \/grafana
- \/metrics_dashboard
- #{query_pattern}
- #{anchor_pattern}
+ strong_memoize(:grafana_regex) do
+ regex_for_project_metrics(
+ %r{
+ /grafana
+ /metrics_dashboard
+ }x
)
- }x
+ end
end
# Parses query params out from full url string into hash.
@@ -62,23 +62,30 @@ module Gitlab
private
- def gitlab_pattern
- Regexp.escape(Gitlab.config.gitlab.url)
- end
-
- def project_pattern
- "\/#{Project.reference_pattern}"
+ def regex_for_project_metrics(path_suffix_pattern)
+ %r{
+ (?<url>
+ #{gitlab_host_pattern}
+ #{project_path_pattern}
+ #{OPTIONAL_DASH_PATTERN}
+ #{path_suffix_pattern}
+ #{QUERY_PATTERN}
+ #{ANCHOR_PATTERN}
+ )
+ }x
end
- def query_pattern
- '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
+ def gitlab_host_pattern
+ Regexp.escape(Gitlab.config.gitlab.url)
end
- def anchor_pattern
- '(?<anchor>\#[a-z0-9_-]+)?'
+ def project_path_pattern
+ "\/#{Project.reference_pattern}"
end
end
end
end
end
end
+
+Gitlab::Metrics::Dashboard::Url.extend_if_ee('::EE::Gitlab::Metrics::Dashboard::Url')
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index f207d91235f..53508938c49 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -60,7 +60,7 @@ module Gitlab
end
meta_import_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
- meta_source_tag = tag :meta, name: 'go-source', content: "#{import_prefix} #{project_url} #{project_url}/tree/#{branch}{/dir} #{project_url}/blob/#{branch}{/dir}/{file}#L{line}"
+ meta_source_tag = tag :meta, name: 'go-source', content: "#{import_prefix} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}"
head_tag = content_tag :head, meta_import_tag + meta_source_tag
html_tag = content_tag :html, head_tag + body_tag
[html_tag, 200]
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index c749816cf6a..ca8f4e34802 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -12,20 +12,21 @@ module Gitlab
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
WHITELISTED_GIT_ROUTES = {
- 'projects/git_http' => %w{git_upload_pack git_receive_pack}
+ 'repositories/git_http' => %w{git_upload_pack git_receive_pack}
}.freeze
WHITELISTED_GIT_LFS_ROUTES = {
- 'projects/lfs_api' => %w{batch},
- 'projects/lfs_locks_api' => %w{verify create unlock}
+ 'repositories/lfs_api' => %w{batch},
+ 'repositories/lfs_locks_api' => %w{verify create unlock}
}.freeze
WHITELISTED_GIT_REVISION_ROUTES = {
'projects/compare' => %w{create}
}.freeze
- WHITELISTED_LOGOUT_ROUTES = {
- 'sessions' => %w{destroy}
+ WHITELISTED_SESSION_ROUTES = {
+ 'sessions' => %w{destroy},
+ 'admin/sessions' => %w{create destroy}
}.freeze
GRAPHQL_URL = '/api/graphql'
@@ -89,7 +90,7 @@ module Gitlab
# Overridden in EE module
def whitelisted_routes
- grack_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || logout_route? || graphql_query?
+ grack_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query?
end
def grack_route?
@@ -122,11 +123,12 @@ module Gitlab
WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
- def logout_route?
+ def session_route?
# Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.post? && request.path.end_with?('/users/sign_out')
+ return false unless request.post? && request.path.end_with?('/users/sign_out',
+ '/admin/session', '/admin/session/destroy')
- WHITELISTED_LOGOUT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
+ WHITELISTED_SESSION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def sidekiq_route?
diff --git a/lib/gitlab/patch/active_record_query_cache.rb b/lib/gitlab/patch/active_record_query_cache.rb
deleted file mode 100644
index d6b649cdea7..00000000000
--- a/lib/gitlab/patch/active_record_query_cache.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-# Fixes a bug where the query cache isn't aware of the shared
-# ActiveRecord connection used in tests
-# https://github.com/rails/rails/issues/36587
-
-# To be removed with https://gitlab.com/gitlab-org/gitlab-foss/issues/64413
-
-module Gitlab
- module Patch
- module ActiveRecordQueryCache
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def enable_query_cache!
- @query_cache_enabled[connection_cache_key(current_thread)] = true
- connection.enable_query_cache! if active_connection?
- end
-
- def disable_query_cache!
- @query_cache_enabled.delete connection_cache_key(current_thread)
- connection.disable_query_cache! if active_connection?
- end
-
- def query_cache_enabled
- @query_cache_enabled[connection_cache_key(current_thread)]
- end
-
- def active_connection?
- @thread_cached_conns[connection_cache_key(current_thread)]
- end
-
- private
-
- def current_thread
- @lock_thread || Thread.current
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
- end
- end
-end
diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/lib/gitlab/phabricator_import/base_worker.rb
deleted file mode 100644
index d2c2ef8db48..00000000000
--- a/lib/gitlab/phabricator_import/base_worker.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-# All workers within a Phabricator import should inherit from this worker and
-# implement the `#import` method. The jobs should then be scheduled using the
-# `.schedule` class method instead of `.perform_async`
-#
-# Doing this makes sure that only one job of that type is running at the same time
-# for a certain project. This will avoid deadlocks. When a job is already running
-# we'll wait for it for 10 times 5 seconds to restart. If the running job hasn't
-# finished, by then, we'll retry in 30 seconds.
-#
-# It also makes sure that we keep the import state of the project up to date:
-# - It keeps track of the jobs so we know how many jobs are running for the
-# project
-# - It refreshes the import jid, so it doesn't get cleaned up by the
-# `StuckImportJobsWorker`
-# - It marks the import as failed if a job failed to many times
-# - It marks the import as finished when all remaining jobs are done
-module Gitlab
- module PhabricatorImport
- class BaseWorker
- include ApplicationWorker
- include ProjectImportOptions # This marks the project as failed after too many tries
- include Gitlab::ExclusiveLeaseHelpers
-
- feature_category :importers
-
- class << self
- def schedule(project_id, *args)
- perform_async(project_id, *args)
- add_job(project_id)
- end
-
- def add_job(project_id)
- worker_state(project_id).add_job
- end
-
- def remove_job(project_id)
- worker_state(project_id).remove_job
- end
-
- def worker_state(project_id)
- Gitlab::PhabricatorImport::WorkerState.new(project_id)
- end
- end
-
- def perform(project_id, *args)
- in_lock("#{self.class.name.underscore}/#{project_id}/#{args}", ttl: 2.hours, sleep_sec: 5.seconds) do
- project = Project.find_by_id(project_id)
- next unless project
-
- # Bail if the import job already failed
- next unless project.import_state&.in_progress?
-
- project.import_state.refresh_jid_expiration
-
- import(project, *args)
-
- # If this is the last running job, finish the import
- project.after_import if self.class.worker_state(project_id).running_count < 2
-
- self.class.remove_job(project_id)
- end
- rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
- # Reschedule a job if there was already a running one
- # Running them at the same time could cause a deadlock updating the same
- # resource
- self.class.perform_in(30.seconds, project_id, *args)
- end
-
- private
-
- def import(project, *args)
- importer_class.new(project, *args).execute
- end
-
- def importer_class
- raise NotImplementedError, "Implement `#{__method__}` on #{self.class}"
- end
- end
- end
-end
diff --git a/lib/gitlab/phabricator_import/import_tasks_worker.rb b/lib/gitlab/phabricator_import/import_tasks_worker.rb
deleted file mode 100644
index c36954a8d41..00000000000
--- a/lib/gitlab/phabricator_import/import_tasks_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-module Gitlab
- module PhabricatorImport
- class ImportTasksWorker < BaseWorker
- def importer_class
- Gitlab::PhabricatorImport::Issues::Importer
- end
- end
- end
-end
diff --git a/lib/gitlab/profiler/total_time_flat_printer.rb b/lib/gitlab/profiler/total_time_flat_printer.rb
index 2c105d2722b..9846bad3c08 100644
--- a/lib/gitlab/profiler/total_time_flat_printer.rb
+++ b/lib/gitlab/profiler/total_time_flat_printer.rb
@@ -24,7 +24,7 @@ module Gitlab
sum += method.self_time
- @output << "%6.2f %9.3f %9.3f %9.3f %9.3f %8d %s%s\n" % [
+ @output << "%6.2f %9.3f %9.3f %9.3f %9.3f %8d %s%-30s %s\n" % [
method.self_time / total_time * 100, # %self
method.total_time, # total
method.self_time, # self
@@ -32,7 +32,8 @@ module Gitlab
method.children_time, # children
method.called, # calls
method.recursive? ? "*" : " ", # cycle
- method_name(method) # name
+ method.full_name, # method_name
+ method_location(method) # location
]
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 2669adb8455..eb7ca80dd60 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,7 +2,7 @@
module Gitlab
class ProjectSearchResults < SearchResults
- attr_reader :project, :repository_ref
+ attr_reader :project, :repository_ref, :per_page
def initialize(current_user, project, query, repository_ref = nil, per_page: 20)
@current_user = current_user
@@ -17,7 +17,7 @@ module Gitlab
when 'notes'
notes.page(page).per(per_page)
when 'blobs'
- paginated_blobs(blobs, page)
+ paginated_blobs(blobs(page), page)
when 'wiki_blobs'
paginated_blobs(wiki_blobs, page)
when 'commits'
@@ -32,7 +32,7 @@ module Gitlab
def formatted_count(scope)
case scope
when 'blobs'
- blobs_count.to_s
+ formatted_limited_count(limited_blobs_count)
when 'notes'
formatted_limited_count(limited_notes_count)
when 'wiki_blobs'
@@ -48,8 +48,8 @@ module Gitlab
super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord
end
- def blobs_count
- @blobs_count ||= blobs.count
+ def limited_blobs_count
+ @limited_blobs_count ||= blobs.count
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -81,7 +81,7 @@ module Gitlab
counts = %i(limited_milestones_count limited_notes_count
limited_merge_requests_count limited_issues_count
- blobs_count wiki_blobs_count)
+ limited_blobs_count wiki_blobs_count)
counts.all? { |count_method| public_send(count_method).zero? } # rubocop:disable GitlabSecurity/PublicSend
end
@@ -95,10 +95,16 @@ module Gitlab
results
end
- def blobs
+ def limit_up_to_page(page)
+ current_page = page&.to_i || 1
+ offset = per_page * (current_page - 1)
+ count_limit + offset
+ end
+
+ def blobs(page = 1)
return [] unless Ability.allowed?(@current_user, :download_code, @project)
- @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query)
+ @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit_up_to_page(page))
end
def wiki_blobs
@@ -106,10 +112,10 @@ module Gitlab
@wiki_blobs ||= begin
if project.wiki_enabled? && query.present?
- unless project.wiki.empty?
- Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query)
- else
+ if project.wiki.empty?
[]
+ else
+ Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query)
end
else
[]
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
index d8f1d1e2316..f5a8aeff73c 100644
--- a/lib/gitlab/project_transfer.rb
+++ b/lib/gitlab/project_transfer.rb
@@ -32,7 +32,7 @@ module Gitlab
private
def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
+ base_dir ||= root_dir
from = File.join(base_dir, path_was)
to = File.join(base_dir, path)
FileUtils.mv(from, to)
diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
index 8873608c411..abc90bad9c3 100644
--- a/lib/gitlab/prometheus/queries/knative_invocation_query.rb
+++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
@@ -20,7 +20,7 @@ module Gitlab
protected
def context(function_id)
- function = Serverless::Function.find_by_id(function_id)
+ function = ::Serverless::Function.find_by_id(function_id)
{
function_name: function.name,
kube_namespace: function.namespace
diff --git a/lib/gitlab/query_limiting/middleware.rb b/lib/gitlab/query_limiting/middleware.rb
index f6ffbfe2645..714fe42884a 100644
--- a/lib/gitlab/query_limiting/middleware.rb
+++ b/lib/gitlab/query_limiting/middleware.rb
@@ -37,10 +37,10 @@ module Gitlab
controller = env[CONTROLLER_KEY]
action = "#{controller.class.name}##{controller.action_name}"
- if controller.content_type == 'text/html'
+ if controller.media_type == 'text/html'
action
else
- "#{action} (#{controller.content_type})"
+ "#{action} (#{controller.media_type})"
end
end
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index e04d6f250b1..6f87968e286 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -34,26 +34,55 @@ module Gitlab
def extract_commands(content, only: nil)
return [content, []] unless content
- content = content.dup
+ content, commands = perform_regex(content, only: only)
- commands = []
+ perform_substitutions(content, commands)
+ end
+ # Encloses quick action commands into code span markdown
+ # avoiding them being executed, for example, when sent via email
+ # to GitLab service desk.
+ # Example: /label ~label1 becomes `/label ~label1`
+ def redact_commands(content)
+ return "" unless content
+
+ content, _ = perform_regex(content, redact: true)
+
+ content
+ end
+
+ private
+
+ def perform_regex(content, only: nil, redact: false)
+ commands = []
+ content = content.dup
content.delete!("\r")
+
content.gsub!(commands_regex(only: only)) do
- if $~[:cmd]
- commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
- ''
- else
- $~[0]
- end
+ command, output = process_commands($~, redact)
+ commands << command
+ output
end
- content, commands = perform_substitutions(content, commands)
-
- [content.rstrip, commands]
+ [content.rstrip, commands.reject(&:empty?)]
end
- private
+ def process_commands(matched_text, redact)
+ output = matched_text[0]
+ command = []
+
+ if matched_text[:cmd]
+ command = [matched_text[:cmd].downcase, matched_text[:arg]].reject(&:blank?)
+ output = ''
+
+ if redact
+ output = "`/#{matched_text[:cmd]}#{" " + matched_text[:arg] if matched_text[:arg]}`"
+ output += "\n" if matched_text[0].include?("\n")
+ end
+ end
+
+ [command, output]
+ end
# Builds a regular expression to match known commands.
# First match group captures the command name and
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 183191f31a6..aff3ed53734 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -12,6 +12,15 @@ module Gitlab
explanation do |users|
_('Assigns %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) }
end
+ execution_message do |users = nil|
+ if users.blank?
+ _("Failed to assign a user because no user was found.")
+ else
+ users = [users.first] unless quick_action_target.allows_multiple_assignees?
+
+ _('Assigned %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) }
+ end
+ end
params do
quick_action_target.allows_multiple_assignees? ? '@user1 @user2' : '@user'
end
@@ -23,19 +32,14 @@ module Gitlab
extract_users(assignee_param)
end
command :assign do |users|
- if users.empty?
- @execution_message[:assign] = _("Failed to assign a user because no user was found.")
- next
- end
+ next if users.empty?
if quick_action_target.allows_multiple_assignees?
@updates[:assignee_ids] ||= quick_action_target.assignees.map(&:id)
- @updates[:assignee_ids] += users.map(&:id)
+ @updates[:assignee_ids] |= users.map(&:id)
else
@updates[:assignee_ids] = [users.first.id]
end
-
- @execution_message[:assign] = _('Assigned %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) }
end
desc do
@@ -249,7 +253,7 @@ module Gitlab
def assignees_for_removal(users)
assignees = quick_action_target.assignees
if users.present? && quick_action_target.allows_multiple_assignees?
- assignees & users
+ users
else
assignees
end
diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb
index 0fda056a4fe..b7231aa3a8b 100644
--- a/lib/gitlab/quick_actions/substitution_definition.rb
+++ b/lib/gitlab/quick_actions/substitution_definition.rb
@@ -10,14 +10,14 @@ module Gitlab
end
def match(content)
- content.match %r{^/#{all_names.join('|')} ?(.*)$}
+ content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$}
end
def perform_substitution(context, content)
return unless content
all_names.each do |a_name|
- content = content.gsub(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1'))
+ content = content.gsub(%r{/#{a_name}(?![\S]) ?(.*)$}i, execute_block(action_block, context, '\1'))
end
content
diff --git a/lib/gitlab/redis/boolean.rb b/lib/gitlab/redis/boolean.rb
new file mode 100644
index 00000000000..9b0b20fc2be
--- /dev/null
+++ b/lib/gitlab/redis/boolean.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+# A serializer for boolean values being stored in Redis.
+#
+# This is to ensure that booleans are stored in a consistent and
+# testable way when being stored as strings in Redis.
+#
+# Examples:
+#
+# bool = Gitlab::Redis::Boolean.new(true)
+# bool.to_s == "_b:1"
+#
+# Gitlab::Redis::Boolean.encode(true)
+# => "_b:1"
+#
+# Gitlab::Redis::Boolean.decode("_b:1")
+# => true
+#
+# Gitlab::Redis::Boolean.true?("_b:1")
+# => true
+#
+# Gitlab::Redis::Boolean.true?("_b:0")
+# => false
+
+module Gitlab
+ module Redis
+ class Boolean
+ LABEL = "_b"
+ DELIMITER = ":"
+ TRUE_STR = "1"
+ FALSE_STR = "0"
+
+ BooleanError = Class.new(StandardError)
+ NotABooleanError = Class.new(BooleanError)
+ NotAnEncodedBooleanStringError = Class.new(BooleanError)
+
+ def initialize(value)
+ @value = value
+ end
+
+ # @return [String] the encoded boolean
+ def to_s
+ self.class.encode(@value)
+ end
+
+ class << self
+ # Turn a boolean into a string for storage in Redis
+ #
+ # @param value [Boolean] true or false
+ # @return [String] the encoded boolean
+ # @raise [NotABooleanError] if the value isn't true or false
+ def encode(value)
+ raise NotABooleanError.new(value) unless bool?(value)
+
+ [LABEL, to_string(value)].join(DELIMITER)
+ end
+
+ # Decode a boolean string
+ #
+ # @param value [String] the stored boolean string
+ # @return [Boolean] true or false
+ # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
+ def decode(value)
+ raise NotAnEncodedBooleanStringError.new(value.class) unless value.is_a?(String)
+
+ label, bool_str = *value.split(DELIMITER, 2)
+
+ raise NotAnEncodedBooleanStringError.new(label) unless label == LABEL
+
+ from_string(bool_str)
+ end
+
+ # Decode a boolean string, then test if it's true
+ #
+ # @param value [String] the stored boolean string
+ # @return [Boolean] is the value true?
+ # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
+ def true?(encoded_value)
+ decode(encoded_value)
+ end
+
+ # Decode a boolean string, then test if it's false
+ #
+ # @param value [String] the stored boolean string
+ # @return [Boolean] is the value false?
+ # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean
+ def false?(encoded_value)
+ !true?(encoded_value)
+ end
+
+ private
+
+ def bool?(value)
+ [true, false].include?(value)
+ end
+
+ def to_string(bool)
+ bool ? TRUE_STR : FALSE_STR
+ end
+
+ def from_string(str)
+ raise NotAnEncodedBooleanStringError.new(str) unless [TRUE_STR, FALSE_STR].include?(str)
+
+ str == TRUE_STR
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 48eaf073e8a..fd6e24a96d8 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -5,6 +5,9 @@ module Gitlab
extend self
def project_name_regex
+ # The character range \p{Alnum} overlaps with \u{00A9}-\u{1f9ff}
+ # hence the Ruby warning.
+ # https://gitlab.com/gitlab-org/gitlab/merge_requests/23165#not-easy-fixable
@project_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_\. ]*\z/.freeze
end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 1baa2a9e461..e8c749cac14 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,8 +4,9 @@ module Gitlab
module RepoPath
NotFoundError = Class.new(StandardError)
- def self.parse(repo_path)
- project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+ def self.parse(path)
+ repo_path = path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+ redirected_path = nil
# Detect the repo type based on the path, the first one tried is the project
# type, which does not have a suffix.
@@ -14,10 +15,13 @@ module Gitlab
# type.
# We'll always try to find a project with an empty suffix (for the
# `Gitlab::GlRepository::PROJECT` type.
- next unless project_path.end_with?(type.path_suffix)
+ next unless type.valid?(repo_path)
- project, was_redirected = find_project(project_path.chomp(type.path_suffix))
- redirected_path = project_path if was_redirected
+ # Removing the suffix (.wiki, .design, ...) from the project path
+ full_path = repo_path.chomp(type.path_suffix)
+
+ project, was_redirected = find_project(full_path)
+ redirected_path = repo_path if was_redirected
# If we found a matching project, then the type was matched, no need to
# continue looking.
diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb
index fca8c43da2e..dc8b2467f72 100644
--- a/lib/gitlab/repository_cache.rb
+++ b/lib/gitlab/repository_cache.rb
@@ -33,8 +33,8 @@ module Gitlab
backend.read(cache_key(key))
end
- def write(key, value)
- backend.write(cache_key(key), value)
+ def write(key, value, *args)
+ backend.write(cache_key(key), value, *args)
end
def fetch_without_caching_false(key, &block)
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index b2dc92ce010..304f53b58c4 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -132,6 +132,11 @@ module Gitlab
raise NotImplementedError
end
+ # RepositoryHashCache to be used. Should be overridden by the including class
+ def redis_hash_cache
+ raise NotImplementedError
+ end
+
# List of cached methods. Should be overridden by the including class
def cached_methods
raise NotImplementedError
@@ -215,6 +220,7 @@ module Gitlab
end
expire_redis_set_method_caches(methods)
+ expire_redis_hash_method_caches(methods)
expire_request_store_method_caches(methods)
end
@@ -234,6 +240,10 @@ module Gitlab
methods.each { |name| redis_set_cache.expire(name) }
end
+ def expire_redis_hash_method_caches(methods)
+ methods.each { |name| redis_hash_cache.delete(name) }
+ end
+
# All cached repository methods depend on the existence of a Git repository,
# so if the repository doesn't exist, we already know not to call it.
def fallback_early?(method_name)
diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb
new file mode 100644
index 00000000000..d2a7b450000
--- /dev/null
+++ b/lib/gitlab/repository_hash_cache.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+# Interface to the Redis-backed cache store for keys that use a Redis HSET.
+# This is currently used as an incremental cache by the `Repository` model
+# for `#merged_branch_names`. It works slightly differently to the other
+# repository cache classes in that it is intended to work with partial
+# caches which can be updated with new data, using the Redis hash system.
+
+module Gitlab
+ class RepositoryHashCache
+ attr_reader :repository, :namespace, :expires_in
+
+ RepositoryHashCacheError = Class.new(StandardError)
+ InvalidKeysProvidedError = Class.new(RepositoryHashCacheError)
+ InvalidHashProvidedError = Class.new(RepositoryHashCacheError)
+
+ # @param repository [Repository]
+ # @param extra_namespace [String]
+ # @param expires_in [Integer] expiry time for hash store keys
+ def initialize(repository, extra_namespace: nil, expires_in: 1.day)
+ @repository = repository
+ @namespace = "#{repository.full_path}"
+ @namespace += ":#{repository.project.id}" if repository.project
+ @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
+ @expires_in = expires_in
+ end
+
+ # @param type [String]
+ # @return [String]
+ def cache_key(type)
+ "#{type}:#{namespace}:hash"
+ end
+
+ # @param key [String]
+ # @return [Integer] 0 or 1 depending on success
+ def delete(key)
+ with { |redis| redis.del(cache_key(key)) }
+ end
+
+ # Check if the provided hash key exists in the hash.
+ #
+ # @param key [String]
+ # @param h_key [String] the key to check presence in Redis
+ # @return [True, False]
+ def key?(key, h_key)
+ with { |redis| redis.hexists(cache_key(key), h_key) }
+ end
+
+ # Read the values of a set of keys from the hash store, and return them
+ # as a hash of those keys and their values.
+ #
+ # @param key [String]
+ # @param hash_keys [Array<String>] an array of keys to retrieve from the store
+ # @return [Hash] a Ruby hash of the provided keys and their values from the store
+ def read_members(key, hash_keys = [])
+ raise InvalidKeysProvidedError unless hash_keys.is_a?(Array) && hash_keys.any?
+
+ with do |redis|
+ # Fetch an array of values for the supplied keys
+ values = redis.hmget(cache_key(key), hash_keys)
+
+ # Turn it back into a hash
+ hash_keys.zip(values).to_h
+ end
+ end
+
+ # Write a hash to the store. All keys and values will be strings when stored.
+ #
+ # @param key [String]
+ # @param hash [Hash] the hash to be written to Redis
+ # @return [Boolean] whether all operations were successful or not
+ def write(key, hash)
+ raise InvalidHashProvidedError unless hash.is_a?(Hash) && hash.any?
+
+ full_key = cache_key(key)
+
+ with do |redis|
+ results = redis.pipelined do
+ # Set each hash key to the provided value
+ hash.each do |h_key, h_value|
+ redis.hset(full_key, h_key, h_value)
+ end
+
+ # Update the expiry time for this hset
+ redis.expire(full_key, expires_in)
+ end
+
+ results.all?
+ end
+ end
+
+ # A variation on the `fetch` pattern of other cache stores. This method
+ # allows you to attempt to fetch a group of keys from the hash store, and
+ # for any keys that are missing values a block is then called to provide
+ # those values, which are then written back into Redis. Both sets of data
+ # are then combined and returned as one hash.
+ #
+ # @param key [String]
+ # @param h_keys [Array<String>] the keys to fetch or add to the cache
+ # @yieldparam missing_keys [Array<String>] the keys missing from the cache
+ # @yieldparam new_values [Hash] the hash to be populated by the block
+ # @return [Hash] the amalgamated hash of cached and uncached values
+ def fetch_and_add_missing(key, h_keys, &block)
+ # Check the cache for all supplied keys
+ cache_values = read_members(key, h_keys)
+
+ # Find the results which returned nil (meaning they're not in the cache)
+ missing = cache_values.select { |_, v| v.nil? }.keys
+
+ if missing.any?
+ new_values = {}
+
+ # Run the block, which updates the new_values hash
+ yield(missing, new_values)
+
+ # Ensure all values are converted to strings, to ensure merging hashes
+ # below returns standardised data.
+ new_values = standardize_hash(new_values)
+
+ # Write the new values to the hset
+ write(key, new_values)
+
+ # Merge the two sets of values to return a complete hash
+ cache_values.merge!(new_values)
+ end
+
+ record_metrics(key, cache_values, missing)
+
+ cache_values
+ end
+
+ private
+
+ def with(&blk)
+ Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ # Take a hash and convert both keys and values to strings, for insertion into Redis.
+ #
+ # @param hash [Hash]
+ # @return [Hash] the stringified hash
+ def standardize_hash(hash)
+ hash.map { |k, v| [k.to_s, v.to_s] }.to_h
+ end
+
+ # Record metrics in Prometheus.
+ #
+ # @param key [String] the basic key, e.g. :merged_branch_names. Not record-specific.
+ # @param cache_values [Hash] the hash response from the cache read
+ # @param missing_keys [Array<String>] the array of missing keys from the cache read
+ def record_metrics(key, cache_values, missing_keys)
+ cache_hits = cache_values.delete_if { |_, v| v.nil? }
+
+ # Increment the counter if we have hits
+ metrics_hit_counter.increment(full_hit: missing_keys.empty?, store_type: key) if cache_hits.any?
+
+ # Track the number of hits we got
+ metrics_hit_histogram.observe({ type: "hits", store_type: key }, cache_hits.size)
+ metrics_hit_histogram.observe({ type: "misses", store_type: key }, missing_keys.size)
+ end
+
+ def metrics_hit_counter
+ @counter ||= Gitlab::Metrics.counter(
+ :gitlab_repository_hash_cache_hit,
+ "Count of cache hits in Redis HSET"
+ )
+ end
+
+ def metrics_hit_histogram
+ @histogram ||= Gitlab::Metrics.histogram(
+ :gitlab_repository_hash_cache_size,
+ "Number of records in the HSET"
+ )
+ end
+ end
+end
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index 214670cac28..9da6732796a 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -18,7 +18,6 @@ module Gitlab
def request_deadline
strong_memoize(:request_deadline) do
next unless request_start_time
- next unless Feature.enabled?(:request_deadline)
request_start_time + max_request_duration_seconds
end
diff --git a/lib/gitlab/runtime.rb b/lib/gitlab/runtime.rb
index 97f7a8e2800..bf579dd3b77 100644
--- a/lib/gitlab/runtime.rb
+++ b/lib/gitlab/runtime.rb
@@ -8,15 +8,20 @@ module Gitlab
AmbiguousProcessError = Class.new(IdentificationError)
UnknownProcessError = Class.new(IdentificationError)
+ AVAILABLE_RUNTIMES = [
+ :console,
+ :geo_log_cursor,
+ :puma,
+ :rails_runner,
+ :rake,
+ :sidekiq,
+ :test_suite,
+ :unicorn
+ ].freeze
+
class << self
def identify
- matches = []
- matches << :puma if puma?
- matches << :unicorn if unicorn?
- matches << :console if console?
- matches << :sidekiq if sidekiq?
- matches << :rake if rake?
- matches << :rspec if rspec?
+ matches = AVAILABLE_RUNTIMES.select { |runtime| public_send("#{runtime}?") } # rubocop:disable GitlabSecurity/PublicSend
if matches.one?
matches.first
@@ -48,14 +53,22 @@ module Gitlab
!!(defined?(::Rake) && Rake.application.top_level_tasks.any?)
end
- def rspec?
- Rails.env.test? && process_name == 'rspec'
+ def test_suite?
+ Rails.env.test?
end
def console?
!!defined?(::Rails::Console)
end
+ def geo_log_cursor?
+ !!defined?(::GeoLogCursorOptionParser)
+ end
+
+ def rails_runner?
+ !!defined?(::Rails::Command::RunnerCommand)
+ end
+
def web_server?
puma? || unicorn?
end
@@ -64,17 +77,17 @@ module Gitlab
puma? || sidekiq?
end
- def process_name
- File.basename($0)
- end
-
def max_threads
+ main_thread = 1
+
if puma?
- Puma.cli_config.options[:max_threads]
+ Puma.cli_config.options[:max_threads] + main_thread
elsif sidekiq?
- Sidekiq.options[:concurrency]
+ # An extra thread for the poller in Sidekiq Cron:
+ # https://github.com/ondrejbartas/sidekiq-cron#under-the-hood
+ Sidekiq.options[:concurrency] + main_thread + 1
else
- 1
+ main_thread
end
end
end
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
index 360239a84e4..f472c70446c 100644
--- a/lib/gitlab/search/found_blob.rb
+++ b/lib/gitlab/search/found_blob.rb
@@ -7,6 +7,7 @@ module Gitlab
include Presentable
include BlobLanguageFromGitAttributes
include Gitlab::Utils::StrongMemoize
+ include BlobActiveModel
attr_reader :project, :content_match, :blob_path
diff --git a/lib/gitlab/search/found_wiki_page.rb b/lib/gitlab/search/found_wiki_page.rb
new file mode 100644
index 00000000000..99ca6a79fe2
--- /dev/null
+++ b/lib/gitlab/search/found_wiki_page.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# - rendering by using data purely from Elasticsearch and does not trigger Gitaly calls.
+# - allows policy check
+module Gitlab
+ module Search
+ class FoundWikiPage < SimpleDelegator
+ attr_reader :wiki
+
+ def self.declarative_policy_class
+ 'WikiPagePolicy'
+ end
+
+ # @param found_blob [Gitlab::Search::FoundBlob]
+ def initialize(found_blob)
+ super
+ @wiki = found_blob.project.wiki
+ end
+
+ def to_ability_name
+ 'wiki_page'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/serverless/domain.rb b/lib/gitlab/serverless/domain.rb
new file mode 100644
index 00000000000..ec7c68764d1
--- /dev/null
+++ b/lib/gitlab/serverless/domain.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Serverless
+ class Domain
+ UUID_LENGTH = 14
+
+ def self.generate_uuid
+ SecureRandom.hex(UUID_LENGTH / 2)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/serverless/function_uri.rb b/lib/gitlab/serverless/function_uri.rb
new file mode 100644
index 00000000000..c0e0cf00f35
--- /dev/null
+++ b/lib/gitlab/serverless/function_uri.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Serverless
+ class FunctionURI < URI::HTTPS
+ SERVERLESS_DOMAIN_REGEXP = %r{^(?<scheme>https?://)?(?<function>[^.]+)-(?<cluster_left>\h{2})a1(?<cluster_middle>\h{10})f2(?<cluster_right>\h{2})(?<environment_id>\h+)-(?<environment_slug>[^.]+)\.(?<domain>.+)}.freeze
+
+ attr_reader :function, :cluster, :environment
+
+ def initialize(function: nil, cluster: nil, environment: nil)
+ initialize_required_argument(:function, function)
+ initialize_required_argument(:cluster, cluster)
+ initialize_required_argument(:environment, environment)
+
+ @host = "#{function}-#{cluster.uuid[0..1]}a1#{cluster.uuid[2..-3]}f2#{cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{cluster.domain}"
+
+ super('https', nil, host, nil, nil, nil, nil, nil, nil)
+ end
+
+ def self.parse(uri)
+ match = SERVERLESS_DOMAIN_REGEXP.match(uri)
+ return unless match
+
+ cluster = ::Serverless::DomainCluster.find(match[:cluster_left] + match[:cluster_middle] + match[:cluster_right])
+ return unless cluster
+
+ environment = ::Environment.find(match[:environment_id].to_i(16))
+ return unless environment&.slug == match[:environment_slug]
+
+ new(
+ function: match[:function],
+ cluster: cluster,
+ environment: environment
+ )
+ end
+
+ private
+
+ def initialize_required_argument(name, value)
+ raise ArgumentError.new("missing argument: #{name}") unless value
+
+ instance_variable_set("@#{name}".to_sym, value)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/serverless/service.rb b/lib/gitlab/serverless/service.rb
new file mode 100644
index 00000000000..643e076c587
--- /dev/null
+++ b/lib/gitlab/serverless/service.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+class Gitlab::Serverless::Service
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def name
+ @attributes.dig('metadata', 'name')
+ end
+
+ def namespace
+ @attributes.dig('metadata', 'namespace')
+ end
+
+ def environment_scope
+ @attributes.dig('environment_scope')
+ end
+
+ def environment
+ @attributes.dig('environment')
+ end
+
+ def podcount
+ @attributes.dig('podcount')
+ end
+
+ def created_at
+ strong_memoize(:created_at) do
+ timestamp = @attributes.dig('metadata', 'creationTimestamp')
+ DateTime.parse(timestamp) if timestamp
+ end
+ end
+
+ def image
+ @attributes.dig(
+ 'spec',
+ 'runLatest',
+ 'configuration',
+ 'build',
+ 'template',
+ 'name')
+ end
+
+ def description
+ knative_07_description || knative_05_06_description
+ end
+
+ def cluster
+ @attributes.dig('cluster')
+ end
+
+ def url
+ proxy_url || knative_06_07_url || knative_05_url
+ end
+
+ private
+
+ def proxy_url
+ if cluster&.serverless_domain
+ Gitlab::Serverless::FunctionURI.new(function: name, cluster: cluster.serverless_domain, environment: environment)
+ end
+ end
+
+ def knative_07_description
+ @attributes.dig(
+ 'spec',
+ 'template',
+ 'metadata',
+ 'annotations',
+ 'Description'
+ )
+ end
+
+ def knative_05_06_description
+ @attributes.dig(
+ 'spec',
+ 'runLatest',
+ 'configuration',
+ 'revisionTemplate',
+ 'metadata',
+ 'annotations',
+ 'Description')
+ end
+
+ def knative_05_url
+ domain = @attributes.dig('status', 'domain')
+ return unless domain
+
+ "http://#{domain}"
+ end
+
+ def knative_06_07_url
+ @attributes.dig('status', 'url')
+ end
+end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 290c4cff329..726ecd81824 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -11,18 +11,26 @@ module Gitlab
Error = Class.new(StandardError)
class << self
+ # Retrieve GitLab Shell secret token
+ #
+ # @return [String] secret token
def secret_token
@secret_token ||= begin
File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end
end
+ # Ensure gitlab shell has a secret token stored in the secret_file
+ # if that was never generated, generate a new one
def ensure_secret_token!
return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret'))
generate_and_link_secret_token
end
+ # Returns required GitLab shell version
+ #
+ # @return [String] version from the manifest file
def version_required
@version_required ||= File.read(Rails.root
.join('GITLAB_SHELL_VERSION')).strip
@@ -48,24 +56,31 @@ module Gitlab
end
end
- # Convenience methods for initializing a new repository with a Project model.
+ # Initialize a new project repository using a Project model
+ #
+ # @param [Project] project
+ # @return [Boolean] whether repository could be created
def create_project_repository(project)
create_repository(project.repository_storage, project.disk_path, project.full_path)
end
+ # Initialize a new wiki repository using a Project model
+ #
+ # @param [Project] project
+ # @return [Boolean] whether repository could be created
def create_wiki_repository(project)
create_repository(project.repository_storage, project.wiki.disk_path, project.wiki.full_path)
end
# Init new repository
#
- # storage - the shard key
- # disk_path - project disk path
- # gl_project_path - project name
- #
- # Ex.
+ # @example Create a repository
# create_repository("default", "path/to/gitlab-ci", "gitlab/gitlab-ci")
#
+ # @param [String] storage the shard key
+ # @param [String] disk_path project path on disk
+ # @param [String] gl_project_path project name
+ # @return [Boolean] whether repository could be created
def create_repository(storage, disk_path, gl_project_path)
relative_path = disk_path.dup
relative_path << '.git' unless relative_path.end_with?('.git')
@@ -82,29 +97,39 @@ module Gitlab
false
end
+ # Import wiki repository from external service
+ #
+ # @param [Project] project
+ # @param [Gitlab::LegacyGithubImport::WikiFormatter, Gitlab::BitbucketImport::WikiFormatter] wiki_formatter
+ # @return [Boolean] whether repository could be imported
def import_wiki_repository(project, wiki_formatter)
import_repository(project.repository_storage, wiki_formatter.disk_path, wiki_formatter.import_url, project.wiki.full_path)
end
+ # Import project repository from external service
+ #
+ # @param [Project] project
+ # @return [Boolean] whether repository could be imported
def import_project_repository(project)
import_repository(project.repository_storage, project.disk_path, project.import_url, project.full_path)
end
# Import repository
#
- # storage - project's storage name
- # name - project disk path
- # url - URL to import from
- #
- # Ex.
- # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
+ # @example Import a repository
+ # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git", "gitlab/gitlab-ci")
#
- def import_repository(storage, name, url, gl_project_path)
+ # @param [String] storage project's storage name
+ # @param [String] disk_path project path on disk
+ # @param [String] url from external resource to import from
+ # @param [String] gl_project_path project name
+ # @return [Boolean] whether repository could be imported
+ def import_repository(storage, disk_path, url, gl_project_path)
if url.start_with?('.', '/')
raise Error.new("don't use disk paths with import_repository: #{url.inspect}")
end
- relative_path = "#{name}.git"
+ relative_path = "#{disk_path}.git"
cmd = GitalyGitlabProjects.new(storage, relative_path, gl_project_path)
success = cmd.import_project(url, git_timeout)
@@ -113,27 +138,31 @@ module Gitlab
success
end
- # storage - project's storage path
- # path - project disk path
- # new_path - new project disk path
+ # Move or rename a repository
#
- # Ex.
+ # @example Move/rename a repository
# mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new")
- def mv_repository(storage, path, new_path)
- return false if path.empty? || new_path.empty?
+ #
+ # @param [String] storage project's storage path
+ # @param [String] disk_path current project path on disk
+ # @param [String] new_disk_path new project path on disk
+ # @return [Boolean] whether repository could be moved/renamed on disk
+ def mv_repository(storage, disk_path, new_disk_path)
+ return false if disk_path.empty? || new_disk_path.empty?
- Gitlab::Git::Repository.new(storage, "#{path}.git", nil, nil).rename("#{new_path}.git")
+ Gitlab::Git::Repository.new(storage, "#{disk_path}.git", nil, nil).rename("#{new_disk_path}.git")
true
rescue => e
- Gitlab::ErrorTracking.track_exception(e, path: path, new_path: new_path, storage: storage)
+ Gitlab::ErrorTracking.track_exception(e, path: disk_path, new_path: new_disk_path, storage: storage)
false
end
# Fork repository to new path
- # source_project - forked-from Project
- # target_project - forked-to Project
+ #
+ # @param [Project] source_project forked-from Project
+ # @param [Project] target_project forked-to Project
def fork_repository(source_project, target_project)
forked_from_relative_path = "#{source_project.disk_path}.git"
fork_args = [target_project.repository_storage, "#{target_project.disk_path}.git", target_project.full_path]
@@ -145,29 +174,32 @@ module Gitlab
# for rm_namespace. Given the underlying implementation removes the name
# passed as second argument on the passed storage.
#
- # storage - project's storage path
- # name - project disk path
- #
- # Ex.
+ # @example Remove a repository
# remove_repository("/path/to/storage", "gitlab/gitlab-ci")
- def remove_repository(storage, name)
- return false if name.empty?
+ #
+ # @param [String] storage project's storage path
+ # @param [String] disk_path current project path on disk
+ def remove_repository(storage, disk_path)
+ return false if disk_path.empty?
- Gitlab::Git::Repository.new(storage, "#{name}.git", nil, nil).remove
+ Gitlab::Git::Repository.new(storage, "#{disk_path}.git", nil, nil).remove
true
rescue => e
- Rails.logger.warn("Repository does not exist: #{e} at: #{name}.git") # rubocop:disable Gitlab/RailsLogger
- Gitlab::ErrorTracking.track_exception(e, path: name, storage: storage)
+ Rails.logger.warn("Repository does not exist: #{e} at: #{disk_path}.git") # rubocop:disable Gitlab/RailsLogger
+ Gitlab::ErrorTracking.track_exception(e, path: disk_path, storage: storage)
false
end
# Add new key to authorized_keys
#
- # Ex.
+ # @example Add new key
# add_key("key-42", "sha-rsa ...")
#
+ # @param [String] key_id identifier of the key
+ # @param [String] key_content key content (public certificate)
+ # @return [Boolean] whether key could be added
def add_key(key_id, key_content)
return unless self.authorized_keys_enabled?
@@ -176,39 +208,45 @@ module Gitlab
# Batch-add keys to authorized_keys
#
- # Ex.
+ # @example
# batch_add_keys(Key.all)
+ #
+ # @param [Array<Key>] keys
+ # @return [Boolean] whether keys could be added
def batch_add_keys(keys)
return unless self.authorized_keys_enabled?
gitlab_authorized_keys.batch_add_keys(keys)
end
- # Remove ssh key from authorized_keys
+ # Remove SSH key from authorized_keys
#
- # Ex.
+ # @example Remove a key
# remove_key("key-342")
#
- def remove_key(id, _ = nil)
+ # @param [String] key_id
+ # @return [Boolean] whether key could be removed or not
+ def remove_key(key_id, _ = nil)
return unless self.authorized_keys_enabled?
- gitlab_authorized_keys.rm_key(id)
+ gitlab_authorized_keys.rm_key(key_id)
end
- # Remove all ssh keys from gitlab shell
+ # Remove all SSH keys from gitlab shell
#
- # Ex.
+ # @example Remove all keys
# remove_all_keys
#
+ # @return [Boolean] whether keys could be removed or not
def remove_all_keys
return unless self.authorized_keys_enabled?
gitlab_authorized_keys.clear
end
- # Remove ssh keys from gitlab shell that are not in the DB
+ # Remove SSH keys from gitlab shell that are not in the DB
#
- # Ex.
+ # @example Remove keys not on the database
# remove_keys_not_found_in_db
#
# rubocop: disable CodeReuse/ActiveRecord
@@ -234,11 +272,12 @@ module Gitlab
# Add empty directory for storing repositories
#
- # Ex.
+ # @example Add new namespace directory
# add_namespace("default", "gitlab")
#
+ # @param [String] storage project's storage path
+ # @param [String] name namespace name
def add_namespace(storage, name)
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/58012
Gitlab::GitalyClient.allow_n_plus_1_calls do
Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
end
@@ -249,9 +288,11 @@ module Gitlab
# Remove directory from repositories storage
# Every repository inside this directory will be removed too
#
- # Ex.
+ # @example Remove namespace directory
# rm_namespace("default", "gitlab")
#
+ # @param [String] storage project's storage path
+ # @param [String] name namespace name
def rm_namespace(storage, name)
Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
rescue GRPC::InvalidArgument => e
@@ -261,9 +302,12 @@ module Gitlab
# Move namespace directory inside repositories storage
#
- # Ex.
+ # @example Move/rename a namespace directory
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
+ # @param [String] storage project's storage path
+ # @param [String] old_name current namespace name
+ # @param [String] new_name new namespace name
def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name)
rescue GRPC::InvalidArgument => e
@@ -272,11 +316,17 @@ module Gitlab
false
end
- def url_to_repo(path)
- Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git"
+ # Return a SSH url for a given project path
+ #
+ # @param [String] full_path project path (URL)
+ # @return [String] SSH URL
+ def url_to_repo(full_path)
+ Gitlab.config.gitlab_shell.ssh_path_prefix + "#{full_path}.git"
end
# Return GitLab shell version
+ #
+ # @return [String] version
def version
gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION"
@@ -285,12 +335,23 @@ module Gitlab
end
end
+ # Check if repository exists on disk
+ #
+ # @example Check if repository exists
+ # repository_exists?('default', 'gitlab-org/gitlab.git')
+ #
+ # @return [Boolean] whether repository exists or not
+ # @param [String] storage project's storage path
+ # @param [Object] dir_name repository dir name
def repository_exists?(storage, dir_name)
Gitlab::Git::Repository.new(storage, dir_name, nil, nil).exists?
rescue GRPC::Internal
false
end
+ # Return hooks folder path used by projects
+ #
+ # @return [String] path
def hooks_path
File.join(gitlab_shell_path, 'hooks')
end
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index b246c507e9e..4e0d3da1868 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -4,6 +4,20 @@ require 'yaml'
module Gitlab
module SidekiqConfig
+ FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml'
+ EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml'
+ SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml'
+
+ QUEUE_CONFIG_PATHS = [
+ FOSS_QUEUE_CONFIG_PATH,
+ (EE_QUEUE_CONFIG_PATH if Gitlab.ee?)
+ ].compact.freeze
+
+ DEFAULT_WORKERS = [
+ DummyWorker.new('default', weight: 1),
+ DummyWorker.new('mailers', weight: 2)
+ ].map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze
+
class << self
include Gitlab::SidekiqConfig::CliMethods
@@ -14,7 +28,7 @@ module Gitlab
def config_queues
@config_queues ||= begin
- config = YAML.load_file(Rails.root.join('config/sidekiq_queues.yml'))
+ config = YAML.load_file(Rails.root.join(SIDEKIQ_QUEUES_PATH))
config[:queues].map(&:first)
end
end
@@ -25,28 +39,69 @@ module Gitlab
def workers
@workers ||= begin
- result = find_workers(Rails.root.join('app', 'workers'))
- result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'))) if Gitlab.ee?
+ result = []
+ result.concat(DEFAULT_WORKERS)
+ result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false))
+
+ if Gitlab.ee?
+ result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true))
+ end
+
result
end
end
+ def workers_for_all_queues_yml
+ workers.partition(&:ee?).reverse.map(&:sort)
+ end
+
+ # YAML.load_file is OK here as we control the file contents
+ def all_queues_yml_outdated?
+ foss_workers, ee_workers = workers_for_all_queues_yml
+
+ return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH)
+
+ Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
+ end
+
+ def queues_for_sidekiq_queues_yml
+ namespaces_with_equal_weights =
+ workers
+ .group_by(&:queue_namespace)
+ .map(&:last)
+ .select { |workers| workers.map(&:get_weight).uniq.count == 1 }
+ .map(&:first)
+
+ namespaces = namespaces_with_equal_weights.map(&:queue_namespace).to_set
+ remaining_queues = workers.reject { |worker| namespaces.include?(worker.queue_namespace) }
+
+ (namespaces_with_equal_weights.map(&:namespace_and_weight) +
+ remaining_queues.map(&:queue_and_weight)).sort
+ end
+
+ # YAML.load_file is OK here as we control the file contents
+ def sidekiq_queues_yml_outdated?
+ config_queues = YAML.load_file(SIDEKIQ_QUEUES_PATH)[:queues]
+
+ queues_for_sidekiq_queues_yml != config_queues
+ end
+
private
- def find_workers(root)
+ def find_workers(root, ee:)
concerns = root.join('concerns').to_s
- workers = Dir[root.join('**', '*.rb')]
+ Dir[root.join('**', '*.rb')]
.reject { |path| path.start_with?(concerns) }
+ .map { |path| worker_from_path(path, root) }
+ .select { |worker| worker < Sidekiq::Worker }
+ .map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) }
+ end
- workers.map! do |path|
- ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
-
- ns.camelize.constantize
- end
+ def worker_from_path(path, root)
+ ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
- # Skip things that aren't workers
- workers.select { |w| w < Sidekiq::Worker }
+ ns.camelize.constantize
end
end
end
diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb
index 1ce46289e81..8f19b557d24 100644
--- a/lib/gitlab/sidekiq_config/cli_methods.rb
+++ b/lib/gitlab/sidekiq_config/cli_methods.rb
@@ -18,7 +18,25 @@ module Gitlab
result
end.freeze
- def worker_queues(rails_path = Rails.root.to_s)
+ QUERY_OR_OPERATOR = '|'
+ QUERY_AND_OPERATOR = '&'
+ QUERY_CONCATENATE_OPERATOR = ','
+ QUERY_TERM_REGEX = %r{^(\w+)(!?=)([\w#{QUERY_CONCATENATE_OPERATOR}]+)}.freeze
+
+ QUERY_PREDICATES = {
+ feature_category: :to_sym,
+ has_external_dependencies: lambda { |value| value == 'true' },
+ latency_sensitive: lambda { |value| value == 'true' },
+ name: :to_s,
+ resource_boundary: :to_sym
+ }.freeze
+
+ QueryError = Class.new(StandardError)
+ InvalidTerm = Class.new(QueryError)
+ UnknownOperator = Class.new(QueryError)
+ UnknownPredicate = Class.new(QueryError)
+
+ def all_queues(rails_path = Rails.root.to_s)
@worker_queues ||= {}
@worker_queues[rails_path] ||= QUEUE_CONFIG_PATHS.flat_map do |path|
@@ -27,6 +45,12 @@ module Gitlab
File.exist?(full_path) ? YAML.load_file(full_path) : []
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def worker_queues(rails_path = Rails.root.to_s)
+ # https://gitlab.com/gitlab-org/gitlab/issues/199230
+ worker_names(all_queues(rails_path))
+ end
def expand_queues(queues, all_queues = self.worker_queues)
return [] if queues.empty?
@@ -37,7 +61,65 @@ module Gitlab
[queue, *queues_set.grep(/\A#{queue}:/)]
end
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def query_workers(query_string, queues)
+ worker_names(queues.select(&query_string_to_lambda(query_string)))
+ end
+
+ def clear_memoization!
+ if instance_variable_defined?('@worker_queues')
+ remove_instance_variable('@worker_queues')
+ end
+ end
+
+ private
+
+ def worker_names(workers)
+ workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue }
+ end
+
+ def query_string_to_lambda(query_string)
+ or_clauses = query_string.split(QUERY_OR_OPERATOR).map do |and_clauses_string|
+ and_clauses_predicates = and_clauses_string.split(QUERY_AND_OPERATOR).map do |term|
+ predicate_for_term(term)
+ end
+
+ lambda { |worker| and_clauses_predicates.all? { |predicate| predicate.call(worker) } }
+ end
+
+ lambda { |worker| or_clauses.any? { |predicate| predicate.call(worker) } }
+ end
+
+ def predicate_for_term(term)
+ match = term.match(QUERY_TERM_REGEX)
+
+ raise InvalidTerm.new("Invalid term: #{term}") unless match
+
+ _, lhs, op, rhs = *match
+
+ predicate_for_op(op, predicate_factory(lhs, rhs.split(QUERY_CONCATENATE_OPERATOR)))
+ end
+
+ def predicate_for_op(op, predicate)
+ case op
+ when '='
+ predicate
+ when '!='
+ lambda { |worker| !predicate.call(worker) }
+ else
+ # This is unreachable because InvalidTerm will be raised instead, but
+ # keeping it allows to guard against that changing in future.
+ raise UnknownOperator.new("Unknown operator: #{op}")
+ end
+ end
+
+ def predicate_factory(lhs, values)
+ values_block = QUERY_PREDICATES[lhs.to_sym]
+
+ raise UnknownPredicate.new("Unknown predicate: #{lhs}") unless values_block
+
+ lambda { |queue| values.map(&values_block).include?(queue[lhs.to_sym]) }
+ end
end
end
end
diff --git a/lib/gitlab/sidekiq_config/dummy_worker.rb b/lib/gitlab/sidekiq_config/dummy_worker.rb
new file mode 100644
index 00000000000..858ff0db0c9
--- /dev/null
+++ b/lib/gitlab/sidekiq_config/dummy_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqConfig
+ # For queues that don't have explicit workers - default and mailers
+ class DummyWorker
+ attr_accessor :queue
+
+ ATTRIBUTE_METHODS = {
+ feature_category: :get_feature_category,
+ has_external_dependencies: :worker_has_external_dependencies?,
+ latency_sensitive: :latency_sensitive_worker?,
+ resource_boundary: :get_worker_resource_boundary,
+ weight: :get_weight
+ }.freeze
+
+ def initialize(queue, attributes = {})
+ @queue = queue
+ @attributes = attributes
+ end
+
+ def queue_namespace
+ nil
+ end
+
+ ATTRIBUTE_METHODS.each do |attribute, meth|
+ define_method meth do
+ @attributes[attribute]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb
new file mode 100644
index 00000000000..6cbe327e6b2
--- /dev/null
+++ b/lib/gitlab/sidekiq_config/worker.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqConfig
+ class Worker
+ include Comparable
+
+ attr_reader :klass
+ delegate :feature_category_not_owned?, :get_feature_category,
+ :get_weight, :get_worker_resource_boundary,
+ :latency_sensitive_worker?, :queue, :queue_namespace,
+ :worker_has_external_dependencies?,
+ to: :klass
+
+ def initialize(klass, ee:)
+ @klass = klass
+ @ee = ee
+ end
+
+ def ee?
+ @ee
+ end
+
+ def ==(other)
+ to_yaml == case other
+ when self.class
+ other.to_yaml
+ else
+ other
+ end
+ end
+
+ def <=>(other)
+ to_sort <=> other.to_sort
+ end
+
+ # Put namespaced queues first
+ def to_sort
+ [queue_namespace ? 0 : 1, queue]
+ end
+
+ # YAML representation
+ def encode_with(coder)
+ coder.represent_map(nil, to_yaml)
+ end
+
+ def to_yaml
+ {
+ name: queue,
+ feature_category: get_feature_category,
+ has_external_dependencies: worker_has_external_dependencies?,
+ latency_sensitive: latency_sensitive_worker?,
+ resource_boundary: get_worker_resource_boundary,
+ weight: get_weight
+ }
+ end
+
+ def namespace_and_weight
+ [queue_namespace, get_weight]
+ end
+
+ def queue_and_weight
+ [queue, get_weight]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 8e7626b8eb6..b45014d283f 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -6,8 +6,6 @@ require 'active_record/log_subscriber'
module Gitlab
module SidekiqLogging
class StructuredLogger
- MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes
-
def call(job, queue)
started_time = get_time
base_payload = parse_job(job)
@@ -79,13 +77,15 @@ module Gitlab
end
def parse_job(job)
- job = job.dup
+ # Error information from the previous try is in the payload for
+ # displaying in the Sidekiq UI, but is very confusing in logs!
+ job = job.except('error_backtrace', 'error_class', 'error_message')
# Add process id params
job['pid'] = ::Process.pid
job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS']
- job['args'] = limited_job_args(job['args']) if job['args']
+ job['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(job['args']) if job['args']
job
end
@@ -108,21 +108,6 @@ module Gitlab
def current_time
Gitlab::Metrics::System.monotonic_time
end
-
- def limited_job_args(args)
- return unless args.is_a?(Array)
-
- total_length = 0
- limited_args = args.take_while do |arg|
- total_length += arg.to_json.length
-
- total_length <= MAXIMUM_JOB_ARGUMENTS_LENGTH
- end
-
- limited_args.push('...') if total_length > MAXIMUM_JOB_ARGUMENTS_LENGTH
-
- limited_args
- end
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index 3dda244233f..6c27213df49 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -17,7 +17,9 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::BatchLoader
chain.add Labkit::Middleware::Sidekiq::Server
chain.add Gitlab::SidekiqMiddleware::InstrumentationLogger
+ chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware
+ chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
end
end
@@ -28,7 +30,9 @@ module Gitlab
lambda do |chain|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
chain.add Gitlab::SidekiqMiddleware::ClientMetrics
+ chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client
+ chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/client.rb b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb
new file mode 100644
index 00000000000..e227ee654ee
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module AdminMode
+ # Checks if admin mode is enabled for the request creating the sidekiq job
+ # by examining if admin mode has been enabled for the user
+ # If enabled then it injects a job field that persists through the job execution
+ class Client
+ def call(_worker_class, job, _queue, _redis_pool)
+ return yield unless Feature.enabled?(:user_mode_in_session)
+
+ # Admin mode enabled in the original request or in a nested sidekiq job
+ admin_mode_user_id = find_admin_user_id
+
+ if admin_mode_user_id
+ job['admin_mode_user_id'] ||= admin_mode_user_id
+
+ Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}")
+ end
+
+ yield
+ end
+
+ private
+
+ def find_admin_user_id
+ Gitlab::Auth::CurrentUserMode.current_admin&.id ||
+ Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/server.rb b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb
new file mode 100644
index 00000000000..6366867a0fa
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module AdminMode
+ class Server
+ def call(_worker, job, _queue)
+ return yield unless Feature.enabled?(:user_mode_in_session)
+
+ admin_mode_user_id = job['admin_mode_user_id']
+
+ # Do not bypass session if this job was not enabled with admin mode on
+ return yield unless admin_mode_user_id
+
+ Gitlab::Auth::CurrentUserMode.bypass_session!(admin_mode_user_id) do
+ Gitlab::AppLogger.debug("AdminMode::Server bypasses session for admin mode in job: #{job.inspect}")
+
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/client_metrics.rb b/lib/gitlab/sidekiq_middleware/client_metrics.rb
index cd11415b55e..245a1b5e024 100644
--- a/lib/gitlab/sidekiq_middleware/client_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/client_metrics.rb
@@ -9,8 +9,10 @@ module Gitlab
@metrics = init_metrics
end
- def call(worker, _job, queue, _redis_pool)
- labels = create_labels(worker.class, queue)
+ def call(worker_class, _job, queue, _redis_pool)
+ # worker_class can either be the string or class of the worker being enqueued.
+ worker_class = worker_class.safe_constantize if worker_class.respond_to?(:safe_constantize)
+ labels = create_labels(worker_class, queue)
@metrics.fetch(ENQUEUED).increment(labels, 1)
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index 9588e9ef19a..fbc34357323 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -10,7 +10,7 @@ module Gitlab
def create_labels(worker_class, queue)
labels = { queue: queue.to_s, latency_sensitive: FALSE_LABEL, external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" }
- return labels unless worker_class.include? WorkerAttributes
+ return labels unless worker_class && worker_class.include?(WorkerAttributes)
labels[:latency_sensitive] = bool_as_label(worker_class.latency_sensitive_worker?)
labels[:external_dependencies] = bool_as_label(worker_class.worker_has_external_dependencies?)
diff --git a/lib/gitlab/sidekiq_middleware/worker_context.rb b/lib/gitlab/sidekiq_middleware/worker_context.rb
new file mode 100644
index 00000000000..897a9211948
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/worker_context.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module WorkerContext
+ private
+
+ def wrap_in_optional_context(context_or_nil, &block)
+ return yield unless context_or_nil
+
+ context_or_nil.use(&block)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/client.rb b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
new file mode 100644
index 00000000000..0eb52179db2
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/worker_context/client.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module WorkerContext
+ class Client
+ include Gitlab::SidekiqMiddleware::WorkerContext
+
+ def call(worker_class_or_name, job, _queue, _redis_pool, &block)
+ worker_class = worker_class_or_name.to_s.safe_constantize
+
+ # Mailers can't be constantized like this
+ return yield unless worker_class
+ return yield unless worker_class.include?(::ApplicationWorker)
+
+ context_for_args = worker_class.context_for_arguments(job['args'])
+
+ wrap_in_optional_context(context_for_args, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/worker_context/server.rb b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
new file mode 100644
index 00000000000..d2d84742c17
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/worker_context/server.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module WorkerContext
+ class Server
+ include Gitlab::SidekiqMiddleware::WorkerContext
+
+ def call(worker, job, _queue, &block)
+ worker_class = worker.class
+
+ # This is not a worker we know about, perhaps from a gem
+ return yield unless worker_class.respond_to?(:get_worker_context)
+
+ # Use the context defined on the class level as a base context
+ wrap_in_optional_context(worker_class.get_worker_context, &block)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/signed_commit.rb b/lib/gitlab/signed_commit.rb
new file mode 100644
index 00000000000..809e0a3f034
--- /dev/null
+++ b/lib/gitlab/signed_commit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class SignedCommit
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(commit)
+ @commit = commit
+
+ if commit.project
+ repo = commit.project.repository.raw_repository
+ @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
+ end
+
+ lazy_signature
+ end
+
+ def signature
+ return unless @commit.has_signature?
+ end
+
+ def signature_text
+ strong_memoize(:signature_text) do
+ @signature_data.itself ? @signature_data[0] : nil
+ end
+ end
+
+ def signed_text
+ strong_memoize(:signed_text) do
+ @signature_data.itself ? @signature_data[1] : nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tab_width.rb b/lib/gitlab/tab_width.rb
new file mode 100644
index 00000000000..d33723a2106
--- /dev/null
+++ b/lib/gitlab/tab_width.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module TabWidth
+ extend self
+
+ MIN = 1
+ MAX = 12
+ DEFAULT = 8
+
+ def css_class_for_user(user)
+ return css_class_for_value(DEFAULT) unless user
+
+ css_class_for_value(user.tab_width)
+ end
+
+ private
+
+ def css_class_for_value(value)
+ raise ArgumentError unless in_range?(value)
+
+ "tab-width-#{value}"
+ end
+
+ def in_range?(value)
+ (MIN..MAX).cover?(value)
+ end
+ end
+end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 14c9a3e0389..50e09bdcdd6 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -33,7 +33,7 @@ module Gitlab
self
end
- def to_json
+ def to_json(*)
{ key: key, name: name, content: content }
end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index 63860b9cb26..5992f24f4e9 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -77,6 +77,10 @@ module Gitlab
end
end
+ def self.valid_ids
+ THEMES.map(&:id)
+ end
+
private
def default_id
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e00b49b9042..6e29a3e4cc4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -67,8 +67,8 @@ module Gitlab
clusters_disabled: count(::Clusters::Cluster.disabled),
project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
- clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled),
- clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
+ clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled, batch: false),
+ clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled, batch: false),
clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
clusters_applications_helm: count(::Clusters::Applications::Helm.available),
clusters_applications_ingress: count(::Clusters::Applications::Ingress.available),
@@ -78,14 +78,16 @@ module Gitlab
clusters_applications_runner: count(::Clusters::Applications::Runner.available),
clusters_applications_knative: count(::Clusters::Applications::Knative.available),
clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
+ clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
in_review_folder: count(::Environment.in_review_folder),
grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group),
issues: count(Issue),
issues_created_from_gitlab_error_tracking_ui: count(SentryIssue),
issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
- issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct),
+ issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct, batch: false),
issues_with_embedded_grafana_charts_approx: ::Gitlab::GrafanaEmbedUsageData.issue_count,
+ incident_issues: count(::Issue.authored(::User.alert_bot)),
keys: count(Key),
label_lists: count(List.label),
lfs_objects: count(LfsObject),
@@ -97,6 +99,7 @@ module Gitlab
projects_imported_from_github: count(Project.where(import_type: 'github')),
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
+ projects_with_alerts_service_enabled: count(AlertsService.active, batch: false),
protected_branches: count(ProtectedBranch),
releases: count(Release),
remote_mirrors: count(RemoteMirror),
@@ -178,7 +181,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
- service_counts = count(Service.active.where(template: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1))
+ service_counts = count(Service.active.where(template: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1), batch: false)
results = Service.available_services_names.each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = service_counts["#{service_name}_service".camelize] || 0
@@ -214,9 +217,9 @@ module Gitlab
results[:projects_jira_server_active] += counts[:server].count if counts[:server]
results[:projects_jira_cloud_active] += counts[:cloud].count if counts[:cloud]
if results[:projects_jira_active] == -1
- results[:projects_jira_active] = count(services)
+ results[:projects_jira_active] = count(services, batch: false)
else
- results[:projects_jira_active] += count(services)
+ results[:projects_jira_active] += count(services, batch: false)
end
end
@@ -228,8 +231,22 @@ module Gitlab
{} # augmented in EE
end
- def count(relation, fallback: -1)
- relation.count
+ def count(relation, column = nil, fallback: -1, batch: true)
+ if batch && Feature.enabled?(:usage_ping_batch_counter)
+ Gitlab::Database::BatchCount.batch_count(relation, column)
+ else
+ relation.count
+ end
+ rescue ActiveRecord::StatementInvalid
+ fallback
+ end
+
+ def distinct_count(relation, column = nil, fallback: -1, batch: true)
+ if batch && Feature.enabled?(:usage_ping_batch_counter)
+ Gitlab::Database::BatchCount.batch_distinct_count(relation, column)
+ else
+ relation.distinct_count_by(column)
+ end
rescue ActiveRecord::StatementInvalid
fallback
end
diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb
index ed2ceb8af7c..e185786e638 100644
--- a/lib/gitlab/utils/deep_size.rb
+++ b/lib/gitlab/utils/deep_size.rb
@@ -13,8 +13,8 @@ module Gitlab
def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH)
@root = root
- @max_size = max_size
- @max_depth = max_depth
+ @max_size = max_size || DEFAULT_MAX_SIZE
+ @max_depth = max_depth || DEFAULT_MAX_DEPTH
@size = 0
@depth = 0
diff --git a/lib/gitlab/utils/log_limited_array.rb b/lib/gitlab/utils/log_limited_array.rb
new file mode 100644
index 00000000000..fe8aadf9020
--- /dev/null
+++ b/lib/gitlab/utils/log_limited_array.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Utils
+ module LogLimitedArray
+ MAXIMUM_ARRAY_LENGTH = 10.kilobytes
+
+ # Prepare an array for logging by limiting its JSON representation
+ # to around 10 kilobytes. Once we hit the limit, add "..." as the
+ # last item in the returned array.
+ def self.log_limited_array(array)
+ return [] unless array.is_a?(Array)
+
+ total_length = 0
+ limited_array = array.take_while do |arg|
+ total_length += arg.to_json.length
+
+ total_length <= MAXIMUM_ARRAY_LENGTH
+ end
+
+ limited_array.push('...') if total_length > MAXIMUM_ARRAY_LENGTH
+
+ limited_array
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 29450a33289..8696e23cbc7 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -24,7 +24,7 @@ module Gitlab
attrs = {
GL_ID: Gitlab::GlId.gl_id(user),
- GL_REPOSITORY: repo_type.identifier_for_subject(repository.project),
+ GL_REPOSITORY: repo_type.identifier_for_container(repository.project),
GL_USERNAME: user&.username,
ShowAllRefs: show_all_refs,
Repository: repository.gitaly_repository.to_h,
diff --git a/lib/gitlab/x509/commit.rb b/lib/gitlab/x509/commit.rb
new file mode 100644
index 00000000000..ce298b80a4c
--- /dev/null
+++ b/lib/gitlab/x509/commit.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+require 'openssl'
+require 'digest'
+
+module Gitlab
+ module X509
+ class Commit < Gitlab::SignedCommit
+ def signature
+ super
+
+ return @signature if @signature
+
+ cached_signature = lazy_signature&.itself
+ return @signature = cached_signature if cached_signature.present?
+
+ @signature = create_cached_signature!
+ end
+
+ def update_signature!(cached_signature)
+ cached_signature.update!(attributes)
+ @signature = cached_signature
+ end
+
+ private
+
+ def lazy_signature
+ BatchLoader.for(@commit.sha).batch do |shas, loader|
+ X509CommitSignature.by_commit_sha(shas).each do |signature|
+ loader.call(signature.commit_sha, signature)
+ end
+ end
+ end
+
+ def verified_signature
+ strong_memoize(:verified_signature) { verified_signature? }
+ end
+
+ def cert
+ strong_memoize(:cert) do
+ signer_certificate(p7) if valid_signature?
+ end
+ end
+
+ def cert_store
+ strong_memoize(:cert_store) do
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+ # valid_signing_time? checks the time attributes already
+ # this flag is required, otherwise expired certificates would become
+ # unverified when notAfter within certificate attribute is reached
+ store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME
+ store
+ end
+ end
+
+ def p7
+ strong_memoize(:p7) do
+ pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----')
+ pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----')
+
+ OpenSSL::PKCS7.new(pkcs7_text)
+ rescue
+ nil
+ end
+ end
+
+ def valid_signing_time?
+ # rfc 5280 - 4.1.2.5 Validity
+ # check if signed_time is within the time range (notBefore/notAfter)
+ # non-rfc - git specific check: signed_time >= commit_time
+ p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) &&
+ p7.signers[0].signed_time >= @commit.created_at
+ end
+
+ def valid_signature?
+ p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY)
+ rescue
+ nil
+ end
+
+ def verified_signature?
+ # verify has multiple options but only a boolean return value
+ # so first verify without certificate chain
+ if valid_signature?
+ if valid_signing_time?
+ # verify with system certificate chain
+ p7.verify([], cert_store, signed_text)
+ else
+ false
+ end
+ else
+ nil
+ end
+ rescue
+ nil
+ end
+
+ def signer_certificate(p7)
+ p7.certificates.each do |cert|
+ next if cert.serial != p7.signers[0].serial
+
+ return cert
+ end
+ end
+
+ def certificate_crl
+ extension = get_certificate_extension('crlDistributionPoints')
+ extension.split('URI:').each do |item|
+ item.strip
+
+ if item.start_with?("http")
+ return item.strip
+ end
+ end
+ end
+
+ def get_certificate_extension(extension)
+ cert.extensions.each do |ext|
+ if ext.oid == extension
+ return ext.value
+ end
+ end
+ end
+
+ def issuer_subject_key_identifier
+ get_certificate_extension('authorityKeyIdentifier').gsub("keyid:", "").delete!("\n")
+ end
+
+ def certificate_subject_key_identifier
+ get_certificate_extension('subjectKeyIdentifier')
+ end
+
+ def certificate_issuer
+ cert.issuer.to_s(OpenSSL::X509::Name::RFC2253)
+ end
+
+ def certificate_subject
+ cert.subject.to_s(OpenSSL::X509::Name::RFC2253)
+ end
+
+ def certificate_email
+ get_certificate_extension('subjectAltName').split('email:')[1]
+ end
+
+ def issuer_attributes
+ return if verified_signature.nil?
+
+ {
+ subject_key_identifier: issuer_subject_key_identifier,
+ subject: certificate_issuer,
+ crl_url: certificate_crl
+ }
+ end
+
+ def certificate_attributes
+ return if verified_signature.nil?
+
+ issuer = X509Issuer.safe_create!(issuer_attributes)
+
+ {
+ subject_key_identifier: certificate_subject_key_identifier,
+ subject: certificate_subject,
+ email: certificate_email,
+ serial_number: cert.serial,
+ x509_issuer_id: issuer.id
+ }
+ end
+
+ def attributes
+ return if verified_signature.nil?
+
+ certificate = X509Certificate.safe_create!(certificate_attributes)
+
+ {
+ commit_sha: @commit.sha,
+ project: @commit.project,
+ x509_certificate_id: certificate.id,
+ verification_status: verification_status
+ }
+ end
+
+ def verification_status
+ if verified_signature && certificate_email == @commit.committer_email
+ :verified
+ else
+ :unverified
+ end
+ end
+
+ def create_cached_signature!
+ return if verified_signature.nil?
+
+ return X509CommitSignature.new(attributes) if Gitlab::Database.read_only?
+
+ X509CommitSignature.safe_create!(attributes)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb
index 499ae6111d7..e776e2b7ea3 100644
--- a/lib/gitlab_danger.rb
+++ b/lib/gitlab_danger.rb
@@ -18,7 +18,6 @@ class GitlabDanger
changelog
specs
roulette
- single_codebase
gitlab_ui_wg
ce_ee_vue_templates
].freeze
diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb
index 340bf709f5e..096e1e2ee96 100644
--- a/lib/microsoft_teams/notifier.rb
+++ b/lib/microsoft_teams/notifier.rb
@@ -36,10 +36,7 @@ module MicrosoftTeams
attachments = options[:attachments]
unless attachments.blank?
- result['sections'] << {
- 'title' => 'Details',
- 'facts' => [{ 'name' => 'Attachments', 'value' => attachments }]
- }
+ result['sections'] << { text: attachments }
end
result.to_json
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index db21c0b013b..453b9d21adb 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -63,7 +63,7 @@ module Quality
'get',
RESOURCE_LIST,
%(--namespace "#{namespace}"),
- '-o custom-columns=NAME:.metadata.name'
+ '-o name'
]
run_command(command).lines.map(&:strip)
end
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index 84470a73b1b..85e89059dbb 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -27,11 +27,13 @@ module Quality
policies
presenters
rack_servers
+ replicators
routing
rubocop
serializers
services
sidekiq
+ support_specs
tasks
uploaders
validators
diff --git a/lib/rspec_flaky/report.rb b/lib/rspec_flaky/report.rb
index 9a0fb88c424..73f30362cfe 100644
--- a/lib/rspec_flaky/report.rb
+++ b/lib/rspec_flaky/report.rb
@@ -10,7 +10,7 @@ module RspecFlaky
# This class is responsible for loading/saving JSON reports, and pruning
# outdated examples.
class Report < SimpleDelegator
- OUTDATED_DAYS_THRESHOLD = 90
+ OUTDATED_DAYS_THRESHOLD = 7
attr_reader :flaky_examples
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 8898960c24d..36ec1caf80c 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -54,6 +54,12 @@ module Sentry
end
end
+ def http_post(url, params = {})
+ http_request do
+ Gitlab::HTTP.post(url, **request_params.merge(body: params.to_json))
+ end
+ end
+
def http_request
response = handle_request_exceptions do
yield
diff --git a/lib/sentry/client/issue_link.rb b/lib/sentry/client/issue_link.rb
index 200b1a6b435..91498c19f8b 100644
--- a/lib/sentry/client/issue_link.rb
+++ b/lib/sentry/client/issue_link.rb
@@ -3,8 +3,22 @@
module Sentry
class Client
module IssueLink
- def create_issue_link(integration_id, sentry_issue_identifier, issue)
- issue_link_url = issue_link_api_url(integration_id, sentry_issue_identifier)
+ # Creates a link in Sentry corresponding to the provided
+ # Sentry issue and GitLab issue
+ # @param integration_id [Integer, nil] Representing a global
+ # GitLab integration in Sentry. Nil for plugins.
+ # @param sentry_issue_id [Integer] Id for an issue from Sentry
+ # @param issue [Issue] Issue for which the link should be created
+ def create_issue_link(integration_id, sentry_issue_id, issue)
+ return create_plugin_link(sentry_issue_id, issue) unless integration_id
+
+ create_global_integration_link(integration_id, sentry_issue_id, issue)
+ end
+
+ private
+
+ def create_global_integration_link(integration_id, sentry_issue_id, issue)
+ issue_link_url = global_integration_link_api_url(integration_id, sentry_issue_id)
params = {
project: issue.project.id,
@@ -14,11 +28,22 @@ module Sentry
http_put(issue_link_url, params)
end
- private
+ def global_integration_link_api_url(integration_id, sentry_issue_id)
+ issue_link_url = URI(url)
+ issue_link_url.path = "/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/"
+
+ issue_link_url
+ end
+
+ def create_plugin_link(sentry_issue_id, issue)
+ issue_link_url = plugin_link_api_url(sentry_issue_id)
+
+ http_post(issue_link_url, issue_id: issue.iid)
+ end
- def issue_link_api_url(integration_id, sentry_issue_identifier)
+ def plugin_link_api_url(sentry_issue_id)
issue_link_url = URI(url)
- issue_link_url.path = "/api/0/groups/#{sentry_issue_identifier}/integrations/#{integration_id}/"
+ issue_link_url.path = "/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/"
issue_link_url
end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index bccb94ff0bf..1c51288adf6 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -57,7 +57,7 @@ gitaly_log="$app_root/log/gitaly.log"
# Read configuration variable file if it is present
test -f /etc/default/gitlab && . /etc/default/gitlab
-# Switch to the app_user if it is not he/she who is running the script.
+# Switch to the app_user if it is not they who are running the script.
if [ `whoami` != "$app_user" ]; then
eval su - "$app_user" -c $(echo \")$shell_path -l -c \'$0 "$@"\'$(echo \"); exit;
fi
@@ -340,7 +340,7 @@ start_gitlab() {
# Wait for the pids to be planted
wait_for_pids
- # Finally check the status to tell wether or not GitLab is running
+ # Finally check the status to tell whether or not GitLab is running
print_status
}
diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb
index 07d479848fe..8fbfe7af713 100644
--- a/lib/system_check/helpers.rb
+++ b/lib/system_check/helpers.rb
@@ -57,11 +57,7 @@ module SystemCheck
end
def should_sanitize?
- if ENV['SANITIZE'] == 'true'
- true
- else
- false
- end
+ ENV['SANITIZE'] == 'true'
end
def omnibus_gitlab?
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index cb4d5abffbc..c380eb293b5 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -3,7 +3,7 @@ namespace :cache do
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
- desc "GitLab | Clear redis cache"
+ desc "GitLab | Cache | Clear redis cache"
task redis: :environment do
Gitlab::Redis::Cache.with do |redis|
cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake
index 2f4d11bd942..978a42be638 100644
--- a/lib/tasks/ci/cleanup.rake
+++ b/lib/tasks/ci/cleanup.rake
@@ -1,6 +1,6 @@
namespace :ci do
namespace :cleanup do
- desc "GitLab CI | Clean running builds"
+ desc "GitLab | CI | Clean running builds"
task builds: :environment do
Ci::Build.running.update_all(status: 'canceled')
end
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 0488f26318a..b3ba2434855 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -1,7 +1,7 @@
task dev: ["dev:setup"]
namespace :dev do
- desc "GitLab | Setup developer environment (db, fixtures)"
+ desc "GitLab | Dev | Setup developer environment (db, fixtures)"
task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
@@ -12,7 +12,7 @@ namespace :dev do
Rake::Task["gitlab:shell:setup"].invoke
end
- desc "GitLab | Eager load application"
+ desc "GitLab | Dev | Eager load application"
task load: :environment do
Rails.configuration.eager_load = true
Rails.application.eager_load!
diff --git a/lib/tasks/gitlab/artifacts/migrate.rake b/lib/tasks/gitlab/artifacts/migrate.rake
index 0d09fd0a4e3..871fdfb4fde 100644
--- a/lib/tasks/gitlab/artifacts/migrate.rake
+++ b/lib/tasks/gitlab/artifacts/migrate.rake
@@ -1,7 +1,7 @@
require 'logger'
require 'resolv-replace'
-desc "GitLab | Migrate files for artifacts to comply with new storage format"
+desc 'GitLab | Artifacts | Migrate files for artifacts to comply with new storage format'
namespace :gitlab do
namespace :artifacts do
task migrate: :environment do
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 3aa1dc403d6..b398bbe403f 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -8,7 +8,6 @@ namespace :gitlab do
yarn:check
gettext:po_to_json
rake:assets:precompile
- gitlab:assets:vendor
webpack:compile
gitlab:assets:fix_urls
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 2bf71701b57..8f34101ea15 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -3,18 +3,18 @@ require 'active_record/fixtures'
namespace :gitlab do
namespace :backup do
# Create backup of GitLab system
- desc "GitLab | Create a backup of the GitLab system"
+ desc 'GitLab | Backup | Create a backup of the GitLab system'
task create: :gitlab_environment do
warn_user_is_not_gitlab
- Rake::Task["gitlab:backup:db:create"].invoke
- Rake::Task["gitlab:backup:repo:create"].invoke
- Rake::Task["gitlab:backup:uploads:create"].invoke
- Rake::Task["gitlab:backup:builds:create"].invoke
- Rake::Task["gitlab:backup:artifacts:create"].invoke
- Rake::Task["gitlab:backup:pages:create"].invoke
- Rake::Task["gitlab:backup:lfs:create"].invoke
- Rake::Task["gitlab:backup:registry:create"].invoke
+ Rake::Task['gitlab:backup:db:create'].invoke
+ Rake::Task['gitlab:backup:repo:create'].invoke
+ Rake::Task['gitlab:backup:uploads:create'].invoke
+ Rake::Task['gitlab:backup:builds:create'].invoke
+ Rake::Task['gitlab:backup:artifacts:create'].invoke
+ Rake::Task['gitlab:backup:pages:create'].invoke
+ Rake::Task['gitlab:backup:lfs:create'].invoke
+ Rake::Task['gitlab:backup:registry:create'].invoke
backup = Backup::Manager.new(progress)
backup.pack
@@ -28,7 +28,7 @@ namespace :gitlab do
end
# Restore backup of GitLab system
- desc 'GitLab | Restore a previously created backup'
+ desc 'GitLab | Backup | Restore a previously created backup'
task restore: :gitlab_environment do
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index c0d6cc8ca8e..56cbbae1f67 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :import do
- desc "GitLab | Add all users to all projects (admin users are added as maintainers)"
+ desc "GitLab | Import | Add all users to all projects (admin users are added as maintainers)"
task all_users_to_all_projects: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
@@ -13,7 +13,7 @@ namespace :gitlab do
ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MAINTAINER)
end
- desc "GitLab | Add a specific user to all projects (as a developer)"
+ desc "GitLab | Import | Add a specific user to all projects (as a developer)"
task :user_to_projects, [:email] => :environment do |t, args|
user = User.find_by(email: args.email)
project_ids = Project.pluck(:id)
@@ -21,7 +21,7 @@ namespace :gitlab do
ProjectMember.add_users_to_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER)
end
- desc "GitLab | Add all users to all groups (admin users are added as owners)"
+ desc "GitLab | Import | Add all users to all groups (admin users are added as owners)"
task all_users_to_all_groups: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
@@ -35,7 +35,7 @@ namespace :gitlab do
end
end
- desc "GitLab | Add a specific user to all groups (as a developer)"
+ desc "GitLab | Import | Add a specific user to all groups (as a developer)"
task :user_to_groups, [:email] => :environment do |t, args|
user = User.find_by_email args.email
groups = Group.all
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index b594f150c3b..9e60a585330 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -5,35 +5,35 @@ namespace :gitlab do
end
namespace :app do
- desc 'GitLab | Check the configuration of the GitLab Rails app'
+ desc 'GitLab | App | Check the configuration of the GitLab Rails app'
task check: :gitlab_environment do
SystemCheck::RakeTask::AppTask.run!
end
end
namespace :gitlab_shell do
- desc "GitLab | Check the configuration of GitLab Shell"
+ desc 'GitLab | GitLab Shell | Check the configuration of GitLab Shell'
task check: :gitlab_environment do
SystemCheck::RakeTask::GitlabShellTask.run!
end
end
namespace :gitaly do
- desc 'GitLab | Check the health of Gitaly'
+ desc 'GitLab | Gitaly | Check the health of Gitaly'
task check: :gitlab_environment do
SystemCheck::RakeTask::GitalyTask.run!
end
end
namespace :sidekiq do
- desc "GitLab | Check the configuration of Sidekiq"
+ desc 'GitLab | Sidekiq | Check the configuration of Sidekiq'
task check: :gitlab_environment do
SystemCheck::RakeTask::SidekiqTask.run!
end
end
namespace :incoming_email do
- desc "GitLab | Check the configuration of Reply by email"
+ desc 'GitLab | Incoming Email | Check the configuration of Reply by email'
task check: :gitlab_environment do
SystemCheck::RakeTask::IncomingEmailTask.run!
end
@@ -48,17 +48,17 @@ namespace :gitlab do
end
namespace :orphans do
- desc 'Gitlab | Check for orphaned namespaces and repositories'
+ desc 'Gitlab | Orphans | Check for orphaned namespaces and repositories'
task check: :gitlab_environment do
SystemCheck::RakeTask::OrphansTask.run!
end
- desc 'GitLab | Check for orphaned namespaces in the repositories path'
+ desc 'GitLab | Orphans | Check for orphaned namespaces in the repositories path'
task check_namespaces: :gitlab_environment do
SystemCheck::RakeTask::Orphans::NamespaceTask.run!
end
- desc 'GitLab | Check for orphaned repositories in the repositories path'
+ desc 'GitLab | Orphans | Check for orphaned repositories in the repositories path'
task check_repositories: :gitlab_environment do
SystemCheck::RakeTask::Orphans::RepositoryTask.run!
end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 1961f64659c..e72c5f51ada 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :db do
- desc 'GitLab | Manually insert schema migration version'
+ desc 'GitLab | DB | Manually insert schema migration version'
task :mark_migration_complete, [:version] => :environment do |_, args|
unless args[:version]
puts "Must specify a migration version as an argument".color(:red)
@@ -22,7 +22,7 @@ namespace :gitlab do
end
end
- desc 'Drop all tables'
+ desc 'GitLab | DB | Drop all tables'
task drop_tables: :environment do
connection = ActiveRecord::Base.connection
@@ -41,7 +41,7 @@ namespace :gitlab do
tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") }
end
- desc 'Configures the database by running migrate, or by loading the schema and seeding if needed'
+ desc 'GitLab | DB | Configures the database by running migrate, or by loading the schema and seeding if needed'
task configure: :environment do
# Check if we have existing db tables
# The schema_migrations table will still exist if drop_tables was called
@@ -55,7 +55,7 @@ namespace :gitlab do
end
end
- desc 'Checks if migrations require downtime or not'
+ desc 'GitLab | DB | Checks if migrations require downtime or not'
task :downtime_check, [:ref] => :environment do |_, args|
abort 'You must specify a Git reference to compare with' unless args[:ref]
@@ -71,7 +71,7 @@ namespace :gitlab do
Gitlab::DowntimeCheck.new.check_and_print(migrations)
end
- desc 'Sets up EE specific database functionality'
+ desc 'GitLab | DB | Sets up EE specific database functionality'
if Gitlab.ee?
task setup_ee: %w[geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate]
diff --git a/lib/tasks/gitlab/exclusive_lease.rake b/lib/tasks/gitlab/exclusive_lease.rake
index 83722bf6d94..63b06d5251a 100644
--- a/lib/tasks/gitlab/exclusive_lease.rake
+++ b/lib/tasks/gitlab/exclusive_lease.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :exclusive_lease do
- desc 'GitLab | Clear existing exclusive leases for specified scope (default: *)'
+ desc 'GitLab | Exclusive Lease | Clear existing exclusive leases for specified scope (default: *)'
task :clear, [:scope] => [:environment] do |_, args|
args[:scope].nil? ? Gitlab::ExclusiveLease.reset_all! : Gitlab::ExclusiveLease.reset_all!(args[:scope])
puts 'All exclusive lease entries were removed.'
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 80de3d2ef51..c63ddb62f2a 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :gitaly do
- desc "GitLab | Install or upgrade gitaly"
+ desc 'GitLab | Gitaly | Install or upgrade gitaly'
task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index f8ce3cd46a8..c73691f3d45 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -21,7 +21,7 @@ namespace :gitlab do
)
namespace :graphql do
- desc 'GitLab | Generate GraphQL docs'
+ desc 'GitLab | GraphQL | Generate GraphQL docs'
task compile_docs: :environment do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
@@ -30,7 +30,7 @@ namespace :gitlab do
puts "Documentation compiled."
end
- desc 'GitLab | Check if GraphQL docs are up to date'
+ desc 'GitLab | GraphQL | Check if GraphQL docs are up to date'
task check_docs: :environment do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
@@ -44,7 +44,7 @@ namespace :gitlab do
end
end
- desc 'GitLab | Check if GraphQL schemas are up to date'
+ desc 'GitLab | GraphQL | Check if GraphQL schemas are up to date'
task check_schema: :environment do
idl_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.graphql'))
json_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.json'))
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index adfcc3cda22..701d40b7929 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
# Notes:
# * The project owner will set to the first administator of the system
# * Existing projects will be skipped
- desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
+ desc "GitLab | Import | Import bare repositories from repositories -> storages into GitLab project instance"
task :repos, [:import_path] => :environment do |_t, args|
unless args.import_path
puts 'Please specify an import path that contains the repositories'.color(:red)
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index 5365bd3920f..adf696350d7 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -1,16 +1,16 @@
namespace :gitlab do
namespace :import_export do
- desc "GitLab | Show Import/Export version"
+ desc 'GitLab | Import/Export | Show Import/Export version'
task version: :environment do
puts "Import/Export v#{Gitlab::ImportExport.version}"
end
- desc "GitLab | Display exported DB structure"
+ desc 'GitLab | Import/Export | Display exported DB structure'
task data: :environment do
puts Gitlab::ImportExport::Config.new.to_h['project_tree'].to_yaml(SortKeys: true)
end
- desc 'GitLab | Bumps the Import/Export version in fixtures and project templates'
+ desc 'GitLab | Import/Export | Bumps the Import/Export version in fixtures and project templates'
task bump_version: :environment do
archives = Dir['vendor/project_templates/*.tar.gz']
archives.push('spec/features/projects/import_export/test_project_export.tar.gz')
diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake
index a88fb88c7ef..c832cba0287 100644
--- a/lib/tasks/gitlab/import_export/import.rake
+++ b/lib/tasks/gitlab/import_export/import.rake
@@ -7,12 +7,12 @@
# 2. Performs Sidekiq job synchronously
#
# @example
-# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz]"
+# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz, true]"
#
namespace :gitlab do
namespace :import_export do
- desc 'EXPERIMENTAL | Import large project archives'
- task :import, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
+ desc 'GitLab | Import/Export | EXPERIMENTAL | Import large project archives'
+ task :import, [:username, :namespace_path, :project_path, :archive_path, :measurement_enabled] => :gitlab_environment do |_t, args|
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing'
@@ -26,7 +26,8 @@ namespace :gitlab do
namespace_path: args.namespace_path,
project_path: args.project_path,
username: args.username,
- file_path: args.archive_path
+ file_path: args.archive_path,
+ measurement_enabled: args.measurement_enabled == 'true'
).import
end
end
@@ -38,6 +39,7 @@ class GitlabProjectImport
@file_path = opts.fetch(:file_path)
@namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path))
@current_user = User.find_by_username(opts.fetch(:username))
+ @measurement_enabled = opts.fetch(:measurement_enabled)
end
def import
@@ -72,21 +74,83 @@ class GitlabProjectImport
RequestStore.clear!
end
+ def with_count_queries(&block)
+ count = 0
+
+ counter_f = ->(name, started, finished, unique_id, payload) {
+ unless payload[:name].in? %w[CACHE SCHEMA]
+ count += 1
+ end
+ }
+
+ ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block)
+
+ puts "Number of sql calls: #{count}"
+ end
+
+ def with_gc_counter
+ gc_counts_before = GC.stat.select { |k, v| k =~ /count/ }
+ yield
+ gc_counts_after = GC.stat.select { |k, v| k =~ /count/ }
+ stats = gc_counts_before.merge(gc_counts_after) { |k, vb, va| va - vb }
+ puts "Total GC count: #{stats[:count]}"
+ puts "Minor GC count: #{stats[:minor_gc_count]}"
+ puts "Major GC count: #{stats[:major_gc_count]}"
+ end
+
+ def with_measure_time
+ timing = Benchmark.realtime do
+ yield
+ end
+
+ time = Time.at(timing).utc.strftime("%H:%M:%S")
+ puts "Time to finish: #{time}"
+ end
+
+ def with_measuring
+ puts "Measuring enabled..."
+ with_gc_counter do
+ with_count_queries do
+ with_measure_time do
+ yield
+ end
+ end
+ end
+ end
+
+ def measurement_enabled?
+ @measurement_enabled != false
+ end
+
# We want to ensure that all Sidekiq jobs are executed
# synchronously as part of that process.
# This ensures that all expensive operations do not escape
# to general Sidekiq clusters/nodes.
- def run_isolated_sidekiq_job
+ def with_isolated_sidekiq_job
Sidekiq::Testing.fake! do
with_request_store do
- @project = create_project
-
- execute_sidekiq_job
+ # If you are attempting to import a large project into a development environment,
+ # you may see Gitaly throw an error about too many calls or invocations.
+ # This is due to a n+1 calls limit being set for development setups (not enforced in production)
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24475#note_283090635
+ # For development setups, this code-path will be excluded from n+1 detection.
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ measurement_enabled? ? with_measuring { yield } : yield
+ end
end
+
true
end
end
+ def run_isolated_sidekiq_job
+ with_isolated_sidekiq_job do
+ @project = create_project
+
+ execute_sidekiq_job
+ end
+ end
+
def create_project
# We are disabling ObjectStorage for `import`
# as it is too slow to handle big archives:
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 8fadadccce9..5809f632c5a 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :env do
- desc "GitLab | Show information about GitLab and its environment"
+ desc 'GitLab | Env | Show information about GitLab and its environment'
task info: :gitlab_environment do
# check if there is an RVM environment
rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake
index 6f11646c841..470a12c39cd 100644
--- a/lib/tasks/gitlab/lfs/migrate.rake
+++ b/lib/tasks/gitlab/lfs/migrate.rake
@@ -1,6 +1,6 @@
require 'logger'
-desc "GitLab | Migrate LFS objects to remote storage"
+desc "GitLab | LFS | Migrate LFS objects to remote storage"
namespace :gitlab do
namespace :lfs do
task migrate: :environment do
diff --git a/lib/tasks/gitlab/metrics.rake b/lib/tasks/gitlab/metrics.rake
index 8a57e400dbe..f2635c96638 100644
--- a/lib/tasks/gitlab/metrics.rake
+++ b/lib/tasks/gitlab/metrics.rake
@@ -1,7 +1,7 @@
# frozen_string_literal: true
namespace :metrics do
- desc "GitLab | Setup common metrics"
+ desc "GitLab | Metrics | Setup common metrics"
task setup_common_metrics: :gitlab_environment do
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
diff --git a/lib/tasks/gitlab/seed/group_seed.rake b/lib/tasks/gitlab/seed/group_seed.rake
new file mode 100644
index 00000000000..bc705c94422
--- /dev/null
+++ b/lib/tasks/gitlab/seed/group_seed.rake
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+# Seed test groups with:
+# 1. 2 Subgroups per level
+# 1. 2 Users & group members per group
+# 1. 2 Epics, 2 Milestones & 2 Projects per group
+# 1. Project issues
+#
+# It also assigns each project's issue with one of group's or ascendants
+# groups milestone & epic.
+#
+# @param subgroups_depth - number of subgroup levels
+# @param username - user creating subgroups (i.e. GitLab admin)
+#
+# @example
+# bundle exec rake "gitlab:seed:group_seed[5, root]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed groups with sub-groups/projects/epics/milestones for Group Import testing'
+ task :group_seed, [:subgroups_depth, :username] => :gitlab_environment do |_t, args|
+ require 'sidekiq/testing'
+
+ GroupSeeder.new(
+ subgroups_depth: args.subgroups_depth,
+ username: args.username
+ ).seed
+ end
+ end
+end
+
+class GroupSeeder
+ PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-test.git'
+
+ attr_reader :all_group_ids
+
+ def initialize(subgroups_depth:, username:)
+ @subgroups_depth = subgroups_depth.to_i
+ @user = User.find_by_username(username)
+ @group_names = Set.new
+ @resource_count = 2
+ @all_groups = {}
+ @all_group_ids = []
+ end
+
+ def seed
+ create_groups
+
+ puts 'Done!'
+ end
+
+ def create_groups
+ create_root_group
+ create_sub_groups
+ create_users_and_members
+ create_epics if Gitlab.ee?
+ create_labels
+ create_milestones
+
+ Sidekiq::Testing.inline! do
+ create_projects
+ end
+ end
+
+ def create_users_and_members
+ all_group_ids.each do |group_id|
+ @resource_count.times do |_|
+ user = create_user
+ create_member(user.id, group_id)
+ end
+ end
+ end
+
+ def create_root_group
+ root_group = ::Groups::CreateService.new(@user, group_params).execute
+
+ track_group_id(1, root_group.id)
+ end
+
+ def create_sub_groups
+ (2..@subgroups_depth).each do |level|
+ parent_level = level - 1
+ current_level = level
+ parent_groups = @all_groups[parent_level]
+
+ parent_groups.each do |parent_id|
+ @resource_count.times do |_|
+ sub_group = ::Groups::CreateService.new(@user, group_params(parent_id: parent_id)).execute
+
+ track_group_id(current_level, sub_group.id)
+ end
+ end
+ end
+ end
+
+ def track_group_id(depth_level, group_id)
+ @all_groups[depth_level] ||= []
+ @all_groups[depth_level] << group_id
+ @all_group_ids << group_id
+ end
+
+ def group_params(parent_id: nil)
+ name = unique_name
+
+ {
+ name: name,
+ path: name,
+ parent_id: parent_id
+ }
+ end
+
+ def unique_name
+ name = ffaker_name
+ name = ffaker_name until @group_names.add?(name)
+ name
+ end
+
+ def ffaker_name
+ FFaker::Lorem.characters(5)
+ end
+
+ def create_user
+ User.create!(
+ username: FFaker::Internet.user_name,
+ name: FFaker::Name.name,
+ email: FFaker::Internet.email,
+ confirmed_at: DateTime.now,
+ password: Devise.friendly_token
+ )
+ end
+
+ def create_member(user_id, group_id)
+ roles = Gitlab::Access.values
+
+ GroupMember.create(user_id: user_id, access_level: roles.sample, source_id: group_id)
+ end
+
+ def create_epics
+ all_group_ids.each do |group_id|
+ @resource_count.times do |_|
+ group = Group.find(group_id)
+
+ epic_params = {
+ title: FFaker::Lorem.sentence(6),
+ description: FFaker::Lorem.paragraphs(3).join("\n\n"),
+ author: group.users.sample,
+ group: group
+ }
+
+ Epic.create!(epic_params)
+ end
+ end
+ end
+
+ def create_labels
+ all_group_ids.each do |group_id|
+ @resource_count.times do |_|
+ group = Group.find(group_id)
+ label_title = FFaker::Product.brand
+
+ Labels::CreateService.new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}").execute(group: group)
+ end
+ end
+ end
+
+ def create_milestones
+ all_group_ids.each do |group_id|
+ @resource_count.times do |i|
+ group = Group.find(group_id)
+
+ milestone_params = {
+ title: "v#{i}.0",
+ description: FFaker::Lorem.sentence,
+ state: [:active, :closed].sample
+ }
+
+ Milestones::CreateService.new(group, group.members.sample, milestone_params).execute
+ end
+ end
+ end
+
+ def create_projects
+ all_group_ids.each do |group_id|
+ group = Group.find(group_id)
+
+ @resource_count.times do |i|
+ _, project_path = PROJECT_URL.split('/')[-2..-1]
+
+ project_path.gsub!('.git', '')
+
+ params = {
+ import_url: PROJECT_URL,
+ namespace_id: group.id,
+ name: project_path.titleize + FFaker::Lorem.characters(10),
+ description: FFaker::Lorem.sentence,
+ visibility_level: 0,
+ skip_disk_validation: true
+ }
+
+ project = nil
+
+ Sidekiq::Worker.skipping_transaction_check do
+ project = ::Projects::CreateService.new(@user, params).execute
+ project.send(:_run_after_commit_queue)
+ project.import_state.send(:_run_after_commit_queue)
+ project.repository.expire_all_method_caches
+ end
+
+ create_project_issues(project)
+ assign_issues_to_epics_and_milestones(project)
+ end
+ end
+ end
+
+ def create_project_issues(project)
+ seeder = Quality::Seeders::Issues.new(project: project)
+ seeder.seed(backfill_weeks: 2, average_issues_per_week: 2)
+ end
+
+ def assign_issues_to_epics_and_milestones(project)
+ group_ids = project.group.self_and_ancestors.map(&:id)
+
+ project.issues.each do |issue|
+ issue_params = {
+ milestone: Milestone.where(group: group_ids).sample
+ }
+
+ issue_params[:epic] = Epic.where(group: group_ids).sample if Gitlab.ee?
+
+ issue.update(issue_params)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index a592015963d..ba3e19caf3b 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :shell do
- desc "GitLab | Install or upgrade gitlab-shell"
+ desc "GitLab | Shell | Install or upgrade gitlab-shell"
task :install, [:repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
@@ -54,12 +54,12 @@ namespace :gitlab do
Gitlab::Shell.ensure_secret_token!
end
- desc "GitLab | Setup gitlab-shell"
+ desc "GitLab | Shell | Setup gitlab-shell"
task setup: :gitlab_environment do
setup
end
- desc "GitLab | Build missing projects"
+ desc "GitLab | Shell | Build missing projects"
task build_missing_projects: :gitlab_environment do
Project.find_each(batch_size: 1000) do |project|
path_to_repo = project.repository.path_to_repo
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
new file mode 100644
index 00000000000..eb3de195626
--- /dev/null
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+return if Rails.env.production?
+
+namespace :gitlab do
+ namespace :sidekiq do
+ def write_yaml(path, banner, object)
+ File.write(path, banner + YAML.dump(object))
+ end
+
+ namespace :all_queues_yml do
+ desc 'GitLab | Sidekiq | Generate all_queues.yml based on worker definitions'
+ task generate: :environment do
+ banner = <<~BANNER
+ # This file is generated automatically by
+ # bin/rake gitlab:sidekiq:all_queues_yml:generate
+ #
+ # Do not edit it manually!
+ BANNER
+
+ foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
+
+ write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers)
+
+ if Gitlab.ee?
+ write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
+ end
+ end
+
+ desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
+ task check: :environment do
+ if Gitlab::SidekiqConfig.all_queues_yml_outdated?
+ raise <<~MSG
+ Changes in worker queues found, please update the metadata by running:
+
+ bin/rake gitlab:sidekiq:all_queues_yml:generate
+
+ Then commit and push the changes from:
+
+ - #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH}
+ - #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH}
+
+ MSG
+ end
+ end
+ end
+
+ namespace :sidekiq_queues_yml do
+ desc 'GitLab | Sidekiq | Generate sidekiq_queues.yml based on worker definitions'
+ task generate: :environment do
+ banner = <<~BANNER
+ # This file is generated automatically by
+ # bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate
+ #
+ # Do not edit it manually!
+ #
+ # This configuration file should be exclusively used to set queue settings for
+ # Sidekiq. Any other setting should be specified using the Sidekiq CLI or the
+ # Sidekiq Ruby API (see config/initializers/sidekiq.rb).
+ #
+ # All the queues to process and their weights. Every queue _must_ have a weight
+ # defined.
+ #
+ # The available weights are as follows
+ #
+ # 1: low priority
+ # 2: medium priority
+ # 3: high priority
+ # 5: _super_ high priority, this should only be used for _very_ important queues
+ #
+ # As per http://stackoverflow.com/a/21241357/290102 the formula for calculating
+ # the likelihood of a job being popped off a queue (given all queues have work
+ # to perform) is:
+ #
+ # chance = (queue weight / total weight of all queues) * 100
+ BANNER
+
+ queues_and_weights = Gitlab::SidekiqConfig.queues_for_sidekiq_queues_yml
+
+ write_yaml(Gitlab::SidekiqConfig::SIDEKIQ_QUEUES_PATH, banner, queues: queues_and_weights)
+ end
+
+ desc 'GitLab | Sidekiq | Validate that sidekiq_queues.yml matches worker definitions'
+ task check: :environment do
+ if Gitlab::SidekiqConfig.sidekiq_queues_yml_outdated?
+ raise <<~MSG
+ Changes in worker queues found, please update the metadata by running:
+
+ bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate
+
+ Then commit and push the changes from:
+
+ - #{Gitlab::SidekiqConfig::SIDEKIQ_QUEUES_PATH}
+
+ MSG
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake
index 6b22499a5c8..6a9e87e1541 100644
--- a/lib/tasks/gitlab/two_factor.rake
+++ b/lib/tasks/gitlab/two_factor.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :two_factor do
- desc "GitLab | Disable Two-factor authentication (2FA) for all users"
+ desc "GitLab | 2FA | Disable Two-factor authentication (2FA) for all users"
task disable_for_all_users: :gitlab_environment do
scope = User.with_two_factor
count = scope.count
@@ -25,12 +25,12 @@ namespace :gitlab do
@rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename'])
end
- desc "Encrypt user OTP secrets with a new encryption key"
+ desc "GitLab | 2FA | Rotate Key | Encrypt user OTP secrets with a new encryption key"
task apply: :environment do |t, args|
rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key'])
end
- desc "Rollback to secrets encrypted with the old encryption key"
+ desc "GitLab | 2FA | Rotate Key | Rollback to secrets encrypted with the old encryption key"
task rollback: :environment do
rotator.rollback!
end
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index 15cec80b6a6..0b98755a77c 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :web_hook do
- desc "GitLab | Adds a webhook to the projects"
+ desc "GitLab | Webhook | Adds a webhook to the projects"
task add: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -20,7 +20,7 @@ namespace :gitlab do
end
end
- desc "GitLab | Remove a webhook from the projects"
+ desc "GitLab | Webhook | Remove a webhook from the projects"
task rm: :environment do
web_hook_url = ENV['URL']
namespace_path = ENV['NAMESPACE']
@@ -44,7 +44,7 @@ namespace :gitlab do
puts "#{count} webhooks were removed."
end
- desc "GitLab | List webhooks"
+ desc "GitLab | Webhook | List webhooks"
task list: :environment do
namespace_path = ENV['NAMESPACE']
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index b917a293095..bae3e4e8001 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
namespace :workhorse do
- desc "GitLab | Install or upgrade gitlab-workhorse"
+ desc "GitLab | Workhorse | Install or upgrade gitlab-workhorse"
task :install, [:dir, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index f912f521dfb..500891df43d 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -138,7 +138,7 @@ class GithubRepos
end
namespace :import do
- desc 'Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)'
+ desc 'GitLab | Import | Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)'
task :github, [:token, :gitlab_username, :project_path] => :environment do |_t, args|
abort 'Project path must be: namespace(s)/project_name'.color(:red) unless args.project_path.include?('/')
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 9a5693e78a2..7a4d09bb6d4 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -4,7 +4,7 @@ unless Rails.env.production?
ENV['STATIC_VERIFICATION'] = 'true'
end
- desc "GitLab | lint | Static verification"
+ desc "GitLab | Lint | Static verification"
task static_verification: %w[
lint:static_verification_env
dev:load
@@ -12,19 +12,19 @@ unless Rails.env.production?
Gitlab::Utils::Override.verify!
end
- desc "GitLab | lint | Lint JavaScript files using ESLint"
+ desc "GitLab | Lint | Lint JavaScript files using ESLint"
task :javascript do
Rake::Task['eslint'].invoke
end
- desc "GitLab | lint | Lint HAML files"
+ desc "GitLab | Lint | Lint HAML files"
task :haml do
Rake::Task['haml_lint'].invoke
rescue RuntimeError # The haml_lint tasks raise a RuntimeError
exit(1)
end
- desc "GitLab | lint | Run several lint checks"
+ desc "GitLab | Lint | Run several lint checks"
task :all do
status = 0
@@ -34,14 +34,17 @@ unless Rails.env.production?
scss_lint
gettext:lint
lint:static_verification
+ gitlab:sidekiq:all_queues_yml:check
]
if Gitlab.ee?
- # This task will fail on CE installations (e.g. gitlab-org/gitlab-foss)
- # since it will detect strings in the locale files that do not exist in
- # the source files. To work around this we will only enable this task on
- # EE installations.
+ # These tasks will fail on FOSS installations
+ # (e.g. gitlab-org/gitlab-foss) since they test against a single
+ # file that is generated by an EE installation, which can
+ # contain values that a FOSS installation won't find. To work
+ # around this we will only enable this task on EE installations.
tasks << 'gettext:updated_check'
+ tasks << 'gitlab:sidekiq:sidekiq_queues_yml:check'
end
tasks.each do |task|
diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake
index eb112434dd9..732dedf4d4f 100644
--- a/lib/tasks/migrate/composite_primary_keys.rake
+++ b/lib/tasks/migrate/composite_primary_keys.rake
@@ -1,12 +1,12 @@
namespace :gitlab do
namespace :db do
- desc 'GitLab | Adds primary keys to tables that only have composite unique keys'
+ desc 'GitLab | DB | Adds primary keys to tables that only have composite unique keys'
task composite_primary_keys_add: :environment do
require Rails.root.join('db/optional_migrations/composite_primary_keys')
CompositePrimaryKeysMigration.new.up
end
- desc 'GitLab | Removes previously added composite primary keys'
+ desc 'GitLab | DB | Removes previously added composite primary keys'
task composite_primary_keys_drop: :environment do
require Rails.root.join('db/optional_migrations/composite_primary_keys')
CompositePrimaryKeysMigration.new.down
diff --git a/lib/tasks/pngquant.rake b/lib/tasks/pngquant.rake
index 56dfd5ed081..ceb4de55373 100644
--- a/lib/tasks/pngquant.rake
+++ b/lib/tasks/pngquant.rake
@@ -53,7 +53,7 @@ namespace :pngquant do
end
end
- desc 'GitLab | pngquant | Compress all documentation PNG images using pngquant'
+ desc 'GitLab | Pngquant | Compress all documentation PNG images using pngquant'
task :compress do
check_executable
@@ -69,7 +69,7 @@ namespace :pngquant do
end
end
- desc 'GitLab | pngquant | Checks that all documentation PNG images have been compressed with pngquant'
+ desc 'GitLab | Pngquant | Checks that all documentation PNG images have been compressed with pngquant'
task :lint do
check_executable
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
index cb9f4c751ed..e281ebd5d60 100644
--- a/lib/tasks/sidekiq.rake
+++ b/lib/tasks/sidekiq.rake
@@ -8,28 +8,28 @@ namespace :sidekiq do
WARNING
end
- desc "[DEPRECATED] GitLab | Stop sidekiq"
+ desc '[DEPRECATED] GitLab | Sidekiq | Stop sidekiq'
task :stop do
deprecation_warning!
system(*%w(bin/background_jobs stop))
end
- desc "[DEPRECATED] GitLab | Start sidekiq"
+ desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq'
task :start do
deprecation_warning!
system(*%w(bin/background_jobs start))
end
- desc '[DEPRECATED] GitLab | Restart sidekiq'
+ desc '[DEPRECATED] GitLab | Sidekiq | Restart sidekiq'
task :restart do
deprecation_warning!
system(*%w(bin/background_jobs restart))
end
- desc "[DEPRECATED] GitLab | Start sidekiq with launchd on Mac OS X"
+ desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq with launchd on Mac OS X'
task :launchd do
deprecation_warning!
diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake
index 32061ad4a57..667d850d2de 100644
--- a/lib/tasks/yarn.rake
+++ b/lib/tasks/yarn.rake
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
namespace :yarn do
desc 'Ensure Yarn is installed'